2
# This file is part of GNU Enterprise.
4
# GNU Enterprise is free software; you can redistribute it
5
# and/or modify it under the terms of the GNU General Public
6
# License as published by the Free Software Foundation; either
7
# version 2, or (at your option) any later version.
9
# GNU Enterprise is distributed in the hope that it will be
10
# useful, but WITHOUT ANY WARRANTY; without even the implied
11
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
12
# PURPOSE. See the GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public
15
# License along with program; see the file COPYING. If not,
16
# write to the Free Software Foundation, Inc., 59 Temple Place
17
# - Suite 330, Boston, MA 02111-1307, USA.
19
# Copyright 2004-2005 Free Software Foundation
22
# pdftable/pdftable.py
26
A class that creates multisection tabular pdf reports.
30
# ============================================================================
32
# ============================================================================
33
# ----------------------------------------------------------------------------
34
# Python standard modules
35
# ----------------------------------------------------------------------------
39
# ----------------------------------------------------------------------------
41
# ----------------------------------------------------------------------------
42
from reportlab.lib import colors
43
from reportlab.lib.units import inch
44
from reportlab.lib.pagesizes import letter, landscape, portrait
45
from reportlab.pdfgen import canvas
46
from reportlab.pdfbase.pdfmetrics import getFont
48
# ============================================================================
50
# ============================================================================
51
# ----------------------------------------------------------------------------
52
# Text alignment constants
53
# ----------------------------------------------------------------------------
58
# ----------------------------------------------------------------------------
59
# Some sample font definitions
61
# TODO: This really needs handled in a more flexible mannor.
62
# ----------------------------------------------------------------------------
63
# Font name Pt Size Vert Spacing
64
dataFont = ('Times-Roman', 10, 11)
65
subtotalFont = ('Times-Bold', 10, 12)
66
tableHeaderFont = ('Times-Bold', 10, 12)
68
titleFont = ('Helvetica-Bold', 14, 14)
69
title2Font = ('Helvetica', 12, 14)
70
title3Font = ('Helvetica-Oblique',12, 14)
71
repeatTitleFont = ('Helvetica-Oblique', 9, 10)
72
footerFont = ('Times-Roman', 9, 10)
74
subtitleFont = ('Times-Bold', 12, 13)
75
subtitleLabelFont = ('Times-Roman', 12, 13)
76
subtitleContinueFont = ('Times-Italic', 10, 13)
78
# ----------------------------------------------------------------------------
79
# Sample Color settings
81
# TODO: This really needs handled in a more flexible mannor.
82
# ----------------------------------------------------------------------------
83
highlightColor = colors.HexColor("0xffffcc")
84
headingColor = subtotalColor = colors.HexColor("0xe6e6ff")
87
# ----------------------------------------------------------------------------
90
# TODO: This really needs handled in a more flexible mannor.
91
# ----------------------------------------------------------------------------
95
# NOTE: I tried not to tie internal logic to the font/color/tracking
96
# values that follow, so *theoretically* you can adjust them to
101
# This is nothing but voodoo guesses...
102
# Greatly depends on pointsize of dataFont
103
# TODO: This should probably be computed based on width of a "0"
104
# TODO: and the width of the report. But, eh, this works for now...
105
maxColsForPortraitNonscaled = 100 # Number of columns before we start scaling down
106
maxColsForPortraitScaled = 120 # Number of columns before we switch to landscape
107
maxColsForLandscapeNonscaled = 140 # Number of columns before landscape + scaling
109
# ============================================================================
111
# ============================================================================
114
# ==========================================================================
115
# Creation/closure functions
116
# ==========================================================================
118
# --------------------------------------------------------------------------
120
# --------------------------------------------------------------------------
121
def __init__(self, file, parameterBox = ()):
123
A simple table based report generator.
125
The class provides the following features.
126
Border support for columns, rows.
127
Automatic adjustments of the data when it is unable to fit on one page.
128
Multiple sections of a report handled automatically.
130
@param file: A python file handle used for output
131
@param parameterBox: Unused?
133
self._titleList = ['Your Report','Your Report']
136
# self.parameterBox = parameterBox
138
self.highlightIndex = 0
140
self._scalingComplete = 0
144
# Time of report session
145
self.timestamp = time.strftime("%m/%d/%Y %I:%M%p", time.localtime())
147
# Will be set by _beginData()
152
self._currentSectionType = "default"
153
self._currentSection = {}
154
self.definedSectionTypes = {}
156
self.columnGap = 6 # in pts, Amount of gap to leave between columns... doesn't need to be too much
158
# --------------------------------------------------------------------------
159
# Finish up the output
160
# --------------------------------------------------------------------------
165
Should be called after all data was sent to the report.
167
self.canvas.showPage()
171
# ==========================================================================
172
# Functions that effect report wide settings
173
# ==========================================================================
175
# --------------------------------------------------------------------------
176
# Sets the title of the report
177
# --------------------------------------------------------------------------
178
def setFullTitle(self,titleList):
180
Sets the title of the report
182
@param titleList: A list containing tuples of the following format
183
(text, font defintion)
184
Font defintions are also tuples in the format
185
(Font name, Pt Size, Vert Spacing) such as...
186
('Times-Roman', 10, 11)
188
self._titleList = titleList
190
# --------------------------------------------------------------------------
191
# Define a column on the report
192
# --------------------------------------------------------------------------
193
def addColumn(self, align, minSize, overflow="", highlight=None,
194
leftBorder=0, rightBorder=0, sectionType="default"):
196
Defines a column on the record for the specified section
198
@param minSize: The minimum size in points of the column.
199
@param overflow: The text that should be printed if the contents of a column are too
200
large for the column width.
201
@param highlight: The color of background to use in this column.
202
@param leftBorder: The width of the left side border in points.
203
@param rightBorder: The width of the right side border in points.
204
@param sectionType: The name of the section to which this column should be added.
207
secType = self.definedSectionTypes[sectionType]
209
self.definedSectionTypes[sectionType] = {'columnSizes' :[],
211
'columnOverflow' :[],
212
'columnHighlight' :[],
214
'columnLeftBorder' :[],
215
'columnRightBorder':[],
218
secType = self.definedSectionTypes[sectionType]
220
secType['columnSizes'].append(minSize)
221
secType['columnAligns'].append(align)
222
secType['columnOverflow'].append(overflow)
223
secType['columnHighlight'].append(highlight)
224
secType['columnLeftBorder'].append(leftBorder)
225
secType['columnRightBorder'].append(rightBorder)
227
# ==========================================================================
228
# Section level functions
229
# ==========================================================================
231
# --------------------------------------------------------------------------
232
# Define a section header
233
# --------------------------------------------------------------------------
234
def addHeader(self, heading, align, startColumn, endColumn,
235
leftBorder=0, rightBorder=0, sectionType="default"):
237
Adds a column header to one or more columns
239
@param heading: The text to display
240
@param align: The alignment to apply to the header text
241
LEFT, RIGHT, CENTER are defined in this module
242
@param startColumn: The starting column number (starts at 0) for the header
243
@param endColumn: The ending column number for the header
244
@param leftBorder: The width of the left side border in points.
245
@param rightBorder: The width of the right side border in points.
246
@param sectionType: The name of the section to which this header should be added.
248
secType = self.definedSectionTypes[sectionType]
249
if endColumn > len(secType['columnSizes'])-1:
250
print "endColumn longer than defined columns"
253
heading = {'text':heading,
257
'leftBorder':leftBorder,
258
'rightBorder':rightBorder,
260
secType['headerList'][-1].append(heading)
262
# --------------------------------------------------------------------------
263
# Add a row to a header
264
# --------------------------------------------------------------------------
265
def addHeaderRow(self, sectionType="default"):
267
Adds a new row to the header. Subsequent calls to addHeader will now
268
apply to this new row.
270
@param sectionType: The name of the section to which this header row will be added.
272
secType = self.definedSectionTypes[sectionType]
273
secType['headerList'].append([])
275
# --------------------------------------------------------------------------
276
# Inform the writer to switch to a new section
277
# --------------------------------------------------------------------------
278
def startNewSection(self, subtitle, sectionType="default", newPage=1):
280
Begins a new report section.
282
@param subtitle: The subtitle to display above the section
283
@param sectionType: The name of the previous defined section to use.
284
@param newPage: If 0 then the new page will be supressed.
285
If 1 (the default) then a new page will be output prior
286
to starting the section.
288
if sectionType != self._currentSectionType:
290
self._currentSection = self.definedSectionTypes[sectionType]
291
self._currentSectionType = sectionType
292
self.subtitle = subtitle
294
self.highlightIndex = 0
302
self.drawSectionHeading()
303
self.drawTableHeader()
305
# ==========================================================================
307
# ==========================================================================
309
# --------------------------------------------------------------------------
311
# --------------------------------------------------------------------------
312
def addRow(self, data, style="Data"):
314
Adds a row of data to the current section
316
@param data: A list of strings containing the data to add to the current section
317
@param style: The format style to use to render the row.
318
These are currently hardcoded into this class and include
319
Data (default), Subtotal, Total
324
self.y -= 4 * self.scale
326
if style in ("Subtotal","Total"):
327
font, size, tracking = subtotalFont
328
fontWidth = self.subtotalFontWidth
330
font, size, tracking = dataFont
331
fontWidth = self.dataFontWidth
333
size = size * self.scale
334
tracking = tracking * self.scale
336
if self.y - tracking < self.miny:
341
highlighted = divmod((self.highlightIndex),4)[1]>1
342
self.highlightIndex += 1
344
self.highlightIndex = 0 # Reset highlighting after subtotals
346
boxy = self.y - (tracking-size)*1.5
349
if style in ("Subtotal","Total"):
350
self.drawHorizBorderedBox(leftmargin, boxy, self.width-leftmargin*2,
351
boxh, subtotalColor, lines=lineWidth * self.scale)
353
self.drawHorizBorderedBox(leftmargin, boxy, self.width-leftmargin*2,
354
boxh, highlightColor, lines=0)
356
canvas.setFont(font, size)
361
# Find the hlx values (used for column highlight and borders)
363
hlx1 = self._currentSection['columnCoords'][i-1][1]+self.columnGap/2.0
367
if i < len(self._currentSection['columnCoords'])-1:
368
hlx2 = self._currentSection['columnCoords'][i+1][0]-self.columnGap/2.0
370
hlx2 = self.width-leftmargin*2
372
# Column highlight support
373
highlightColumn = self._currentSection['columnHighlight'][i]
374
if highlightColumn and not style in ("Subtotal","Total"):
375
if self.highlightIndex==1: # We're on the first column (not 0 due to += above)
376
adjust = lineWidth#*self.scale
380
color = colors.Blacker(highlightColor,.98)
382
color = highlightColumn
384
self.drawHorizBorderedBox(hlx1, boxy - adjust, hlx2-hlx1, boxh,
387
# Column border support
388
leftBorder = self._currentSection['columnLeftBorder'][i]
391
canvas.setLineWidth(leftBorder*self.scale)
392
canvas.line(hlx1, boxy, hlx1, boxy + boxh)
394
rightBorder =self._currentSection['columnRightBorder'][i]
396
canvas.setLineWidth(rightBorder*self.scale)
397
canvas.line(hlx2, boxy, hlx2, boxy + boxh)
400
align= self._currentSection['columnAligns'][i]
401
x1, x2 = self._currentSection['columnCoords'][i]
403
# Clip text, if needed
405
if fontWidth(col, size) > x2-x1:
406
if self._currentSection['columnOverflow'][i]:
407
col = self._currentSection['columnOverflow'][i]
411
path = canvas.beginPath()
412
# Vertical is overkill, but only interested in horizontal
413
path.rect(x1,self.y-tracking, x2-x1, tracking*3)
414
canvas.clipPath(path, stroke=0, fill=0)
417
canvas.drawString(x1,self.y,col)
419
canvas.drawRightString(x2,self.y,col)
420
elif align == CENTER:
421
canvas.drawCentredString(x1+(x2-x1)/2.0,self.y,col)
423
# Restore from clipping
425
canvas.restoreState()
430
# ==========================================================================
432
# ==========================================================================
434
# --------------------------------------------------------------------------
436
# --------------------------------------------------------------------------
439
Private function that creates a new page.
442
self.canvas.showPage()
445
self.y = self.height - topmargin
448
self.drawLargeTitle()
450
self.drawSmallTitle()
452
self.drawTableHeader()
458
# --------------------------------------------------------------------------
460
# --------------------------------------------------------------------------
463
Private function that creates the footer containing the time/page #
466
font, size, tracking = footerFont
467
canvas.setFont(font, size)
468
canvas.drawString(leftmargin, topmargin, self.timestamp)
469
canvas.drawRightString(self.width - leftmargin, topmargin, "Page %s" % self.page)
470
self.miny = topmargin + tracking*2
472
# --------------------------------------------------------------------------
473
# Draw full (first-page) header (Title)
474
# --------------------------------------------------------------------------
475
def drawLargeTitle(self):
477
Private function that creates a full (first page) header on a new page.
480
self.y -= titleFont[2]
481
for text, fontspec in self._titleList:
483
font, size, tracking = fontspec
484
canvas.setFont(font, size)
485
canvas.drawCentredString(self.width/2.0, self.y, text)
487
self.drawSectionHeading()
489
# --------------------------------------------------------------------------
490
# Draw short (non-first-page) header (Title)
491
# --------------------------------------------------------------------------
492
def drawSmallTitle(self):
494
Private function that creates a short ( non first page) header on a new page.
497
font, size, tracking = repeatTitleFont
499
canvas.setFont(font, size)
500
canvas.drawString(leftmargin, self.y, self._titleList[0][0])
501
canvas.drawRightString(self.width - leftmargin, self.y, self._titleList[1][0])
503
self.drawSectionHeading()
505
# --------------------------------------------------------------------------
506
# Draw the section header
507
# --------------------------------------------------------------------------
508
def drawSectionHeading(self):
510
Draws the text that preceeds the section's table.
514
if not self.subtitle:
517
self.y -= subtitleFont[2]
519
font, size, tracking = subtitleLabelFont
521
text = canvas.beginText(leftmargin, self.y)
522
for l in self.subtitle.split():
526
font, size, tracking = subtitleFont
532
text.setFont(font, size)
535
font, size, tracking = subtitleLabelFont
539
font2, size2, tracking2 = subtitleContinueFont
540
text.setFont(font2, size2)
541
text.textOut("(Continued)")
543
canvas.drawText(text)
547
# --------------------------------------------------------------------------
549
# --------------------------------------------------------------------------
550
def drawTableHeader(self):
552
Generates a section's table header.
556
numRows = len(self._currentSection['headerList'])
558
font, size, tracking = tableHeaderFont
559
size = size * self.scale
560
tracking = tracking * self.scale
561
canvas.setFont(font, size)
563
boxy = self.y + tracking - (tracking-size)/2.0
564
boxh = -tracking*numRows - (tracking-size)/2.0 - lineWidth*self.scale
565
self.drawHorizBorderedBox(leftmargin, boxy, self.width-leftmargin*2,
568
for list in self._currentSection['headerList']:
572
x1 = self._currentSection['columnCoords'][c1][0]
573
x2 = self._currentSection['columnCoords'][c2][1]
574
align = header['align']
575
text = header['text']
578
path = canvas.beginPath()
579
# Vertical is overkill, but only interested in horizontal
580
path.rect(x1,self.y-tracking, x2-x1, tracking*3)
581
canvas.clipPath(path, stroke=0, fill=0)
584
canvas.drawString(x1,self.y,text)
586
canvas.drawRightString(x2,self.y,text)
587
elif align == CENTER:
588
canvas.drawCentredString(x1+(x2-x1)/2.0,self.y,text)
589
canvas.restoreState()
591
leftBorder = header['leftBorder']
593
canvas.setLineWidth(leftBorder*self.scale)
594
canvas.line(x1-self.columnGap/2.0, boxy, x1-self.columnGap/2.0, boxy + boxh)
596
rightBorder = header['rightBorder']
598
canvas.setLineWidth(rightBorder*self.scale)
599
canvas.line(x2+self.columnGap/2.0, boxy, x2+self.columnGap/2.0, boxy + boxh)
604
# --------------------------------------------------------------------------
605
# Draws a box w/shading and a top/bottom border
606
# --------------------------------------------------------------------------
607
def drawHorizBorderedBox(self, x, y, w, h, color, lines=lineWidth):
610
canvas.setFillColor(color)
611
canvas.rect(x,y,w,h, stroke=0,fill=1)
613
canvas.setLineWidth(lines)
614
canvas.line(x,y,x+w,y)
615
canvas.line(x,y+h,x+w,y+h)
616
canvas.restoreState()
618
# --------------------------------------------------------------------------
619
# Initialize report section
620
# --------------------------------------------------------------------------
623
Prepares the class to begin drawing a section on the report. Figures out
624
the required orientation of the report as well as any scaling that is required
626
# Calculate column sizes
628
for cs in self._currentSection['columnSizes']:
631
# Figure out the page orientation/scaling
632
if not self._scalingComplete:
633
self._scalingComplete = 1
634
self.pageSize = letter
636
if totalCols < maxColsForPortraitNonscaled: # Guestimate of max # cols we can get on portrait
637
self.pageOrient = portrait
638
self.width, self.height = letter
639
elif totalCols < maxColsForPortraitScaled:
640
self.pageOrient = portrait
641
self.width, self.height = letter
642
self.scale = maxColsForPortraitNonscaled / float(totalCols)
643
elif totalCols < maxColsForLandscapeNonscaled:
644
self.pageOrient = landscape
645
self.height, self.width = letter
647
self.pageOrient = landscape
648
self.height, self.width = letter
649
self.scale = maxColsForLandscapeNonscaled / float(totalCols)
652
print "Scaling to %.2f%%" % (self.scale*100)
654
# in pts, Amount of gap to leave between columns... doesn't need to be too much
655
self.columnGap = self.columnGap * self.scale
657
self.canvas = canvas.Canvas(self.file, pagesize=self.pageOrient(self.pageSize))
659
font, size, leading = dataFont
660
self.dataFontWidth = getFont(font).stringWidth
662
font, size, leading = subtotalFont
663
self.subtotalFontWidth = getFont(font).stringWidth
665
# This is not scaled down according to self.scale...
666
# we'll instead scale the point sizes down when we do setFont
667
usableHorizSpace = (self.width - 2*leftmargin - self.columnGap*(len(self._currentSection['columnSizes'])))
668
x = leftmargin + self.columnGap/2.0
669
for i in range(len(self._currentSection['columnSizes'])):
670
colSize = (self._currentSection['columnSizes'][i] / float(totalCols) * usableHorizSpace)
671
self._currentSection['columnCoords'].append ( ( x, x+colSize ) )
672
x += colSize + self.columnGap