#! /usr/bin/env python # -*- coding: ISO-8859-15 -*- # PyKota Invoice generator # # PyKota - Print Quotas for CUPS and LPRng # # (c) 2003, 2004, 2005, 2006, 2007 Jerome Alet # 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # $Id$ # # import sys import os import pwd import time import cStringIO try : from reportlab.pdfgen import canvas from reportlab.lib import pagesizes from reportlab.lib.units import cm except ImportError : hasRL = 0 else : hasRL = 1 try : import PIL.Image except ImportError : hasPIL = 0 else : hasPIL = 1 from pykota.tool import Percent, PyKotaTool, PyKotaToolError, PyKotaCommandLineError, crashed, N_ __doc__ = N_("""pkinvoice v%(__version__)s (c) %(__years__)s %(__author__)s An invoice generator for PyKota. command line usage : pkinvoice [options] [filterexpr] options : -v | --version Prints pkinvoice's version number then exits. -h | --help Prints this message then exits. -l | --logo img Use the image as the invoice's logo. The logo will be drawn at the center top of the page. The default logo is /usr/share/pykota/logos/pykota.jpeg -p | --pagesize sz Sets sz as the page size. Most well known page sizes are recognized, like 'A4' or 'Letter' to name a few. The default size is A4. -n | --number N Sets the number of the first invoice. This number will automatically be incremented for each invoice. -o | --output f.pdf Defines the name of the invoice file which will be generated as a PDF document. If not set or set to '-', the PDF document is sent to standard output. -u | --unit u Defines the name of the unit to use on the invoice. The default unit is 'Credits', optionally translated to your native language if it is supported by PyKota. -V | --vat p Sets the percent value of the applicable VAT to be exposed. The default is 0.0, meaning no VAT information will be included. Use the filter expressions to extract only parts of the datas. Allowed filters are of the form : key=value Allowed keys for now are : username User's name printername Printer's name hostname Client's hostname jobid Job's Id billingcode Job's billing code start Job's date of printing end Job's date of printing Dates formatting with 'start' and 'end' filter keys : YYYY : year boundaries YYYYMM : month boundaries YYYYMMDD : day boundaries YYYYMMDDhh : hour boundaries YYYYMMDDhhmm : minute boundaries YYYYMMDDhhmmss : second boundaries yesterday[+-NbDays] : yesterday more or less N days (e.g. : yesterday-15) today[+-NbDays] : today more or less N days (e.g. : today-15) tomorrow[+-NbDays] : tomorrow more or less N days (e.g. : tomorrow-15) now[+-NbDays] : now more or less N days (e.g. now-15) 'now' and 'today' are not exactly the same since today represents the first or last second of the day depending on if it's used in a start= or end= date expression. The utility to be able to specify dates in the future is a question which remains to be answered :-) Contrary to other PyKota management tools, wildcard characters are not expanded, so you can't use them. examples : $ pkinvoice --unit EURO --output /tmp/invoices.pdf start=now-30 Will generate a PDF document containing invoices for all users who have spent some credits last month. Invoices will be done in EURO. No VAT information will be included. """) class PKInvoice(PyKotaTool) : """A class for invoice generator.""" validfilterkeys = [ "username", "printername", "hostname", "jobid", "billingcode", "start", "end", ] def getPageSize(self, pgsize) : """Returns the correct page size or None if not found.""" try : return getattr(pagesizes, pgsize.upper()) except AttributeError : try : return getattr(pagesizes, pgsize.lower()) except AttributeError : pass def printVar(self, label, value, size) : """Outputs a variable onto the PDF canvas. Returns the number of points to substract to current Y coordinate. """ xcenter = (self.pagesize[0] / 2.0) - 1*cm self.canvas.saveState() self.canvas.setFont("Helvetica-Bold", size) self.canvas.setFillColorRGB(0, 0, 0) self.canvas.drawRightString(xcenter, self.ypos, "%s :" % self.userCharsetToUTF8(label)) self.canvas.setFont("Courier-Bold", size) self.canvas.setFillColorRGB(0, 0, 1) self.canvas.drawString(xcenter + 0.5*cm, self.ypos, self.userCharsetToUTF8(value)) self.canvas.restoreState() self.ypos -= (size + 4) def pagePDF(self, invoicenumber, name, values, unit, vat) : """Generates a new page in the PDF document.""" amount = values["nbcredits"] if amount : # is there's something due ? ht = ((amount * 10000.0) / (100.0 + vat)) / 100.0 vatamount = amount - ht self.canvas.doForm("background") self.ypos = self.yorigine - (cm + 20) self.printVar(_("Invoice"), "#%s" % invoicenumber, 22) self.printVar(_("Username"), name, 22) self.ypos -= 20 self.printVar(_("Edited on"), time.strftime("%c", time.localtime()), 14) self.ypos -= 20 self.printVar(_("Number of jobs printed"), str(values["nbjobs"]), 18) self.printVar(_("Number of pages printed"), str(values["nbpages"]), 18) self.ypos -= 20 self.printVar(_("Amount due"), "%.3f %s" % (amount, unit), 18) if vat : self.ypos += 8 self.printVar("%s (%.2f%%)" % (_("Included VAT"), vat), "%.3f %s" % (vatamount, unit), 14) self.canvas.showPage() return 1 return 0 def initPDF(self, logo) : """Initializes the PDF document.""" self.pdfDocument = cStringIO.StringIO() self.canvas = c = canvas.Canvas(self.pdfDocument, \ pagesize=self.pagesize, \ pageCompression=1) c.setAuthor(self.originalUserName) c.setTitle("PyKota invoices") c.setSubject("Invoices generated with PyKota") self.canvas.beginForm("background") self.canvas.saveState() self.ypos = self.pagesize[1] - (2 * cm) xcenter = self.pagesize[0] / 2.0 if logo : try : imglogo = PIL.Image.open(logo) except IOError : self.printInfo("Unable to open image %s" % logo, "warn") else : (width, height) = imglogo.size multi = float(width) / (8 * cm) width = float(width) / multi height = float(height) / multi self.ypos -= height c.drawImage(logo, xcenter - (width / 2.0), \ self.ypos, \ width, height) self.ypos -= (cm + 20) self.canvas.setFont("Helvetica-Bold", 14) self.canvas.setFillColorRGB(0, 0, 0) msg = _("Here's the invoice for your printouts") self.canvas.drawCentredString(xcenter, self.ypos, "%s :" % self.userCharsetToUTF8(msg)) self.yorigine = self.ypos self.canvas.restoreState() self.canvas.endForm() def endPDF(self, fname) : """Flushes the PDF generator.""" self.canvas.save() if fname != "-" : outfile = open(fname, "w") outfile.write(self.pdfDocument.getvalue()) outfile.close() else : sys.stdout.write(self.pdfDocument.getvalue()) sys.stdout.flush() def genInvoices(self, peruser, logo, outfname, firstnumber, unit, vat) : """Generates the invoices file.""" if len(peruser) : percent = Percent(self) percent.setSize(len(peruser)) if outfname != "-" : percent.display("%s...\n" % _("Generating invoices")) self.initPDF(logo) number = firstnumber for (name, values) in peruser.items() : number += self.pagePDF(number, name, values, unit, vat) if outfname != "-" : percent.oneMore() if number > firstnumber : self.endPDF(outfname) if outfname != "-" : percent.done() def main(self, arguments, options) : """Generate invoices.""" if not hasRL : raise PyKotaToolError, "The ReportLab module is missing. Download it from http://www.reportlab.org" if not hasPIL : raise PyKotaToolError, "The Python Imaging Library is missing. Download it from http://www.pythonware.com/downloads" if not self.config.isAdmin : raise PyKotaCommandLineError, "%s : %s" % (pwd.getpwuid(os.geteuid())[0], _("You're not allowed to use this command.")) try : vat = float(options["vat"]) if not (0.0 <= vat < 100.0) : raise ValueError except : raise PyKotaCommandLineError, _("Incorrect value '%s' for the --vat command line option") % options["vat"] try : firstnumber = number = int(options["number"]) if number <= 0 : raise ValueError except : raise PyKotaCommandLineError, _("Incorrect value '%s' for the --number command line option") % options["number"] self.pagesize = self.getPageSize(options["pagesize"]) if self.pagesize is None : self.pagesize = self.getPageSize("a4") self.printInfo(_("Invalid 'pagesize' option %s, defaulting to A4.") % options["pagesize"], "warn") extractonly = {} for filterexp in arguments : if filterexp.strip() : try : (filterkey, filtervalue) = [part.strip() for part in filterexp.split("=")] filterkey = filterkey.lower() if filterkey not in self.validfilterkeys : raise ValueError except ValueError : raise PyKotaCommandLineError, _("Invalid filter value [%s], see help.") % filterexp else : extractonly.update({ filterkey : filtervalue }) percent = Percent(self) outfname = options["output"].strip() if outfname != "-" : percent.display("%s..." % _("Extracting datas")) username = extractonly.get("username") if username : user = self.storage.getUser(username) else : user = None printername = extractonly.get("printername") if printername : printer = self.storage.getPrinter(printername) else : printer = None start = extractonly.get("start") end = extractonly.get("end") (start, end) = self.storage.cleanDates(start, end) jobs = self.storage.retrieveHistory(user=user, printer=printer, hostname=extractonly.get("hostname"), billingcode=extractonly.get("billingcode"), jobid=extractonly.get("jobid"), start=start, end=end, limit=0) peruser = {} nbjobs = 0 nbpages = 0 nbcredits = 0.0 percent.setSize(len(jobs)) if outfname != "-" : percent.display("\n") for job in jobs : if job.JobSize and (job.JobAction not in ("DENY", "CANCEL", "REFUND")) : nbpages += job.JobSize nbcredits += job.JobPrice counters = peruser.setdefault(job.UserName, { "nbjobs" : 0, "nbpages" : 0, "nbcredits" : 0.0 }) counters["nbpages"] += job.JobSize counters["nbcredits"] += job.JobPrice counters["nbjobs"] += 1 nbjobs += 1 if outfname != "-" : percent.oneMore() if outfname != "-" : percent.done() self.genInvoices(peruser, options["logo"].strip(), outfname, firstnumber, options["unit"], vat) if outfname != "-" : print _("Invoiced %i users for %i jobs, %i pages and %.3f credits") % (len(peruser), nbjobs, nbpages, nbcredits) if __name__ == "__main__" : retcode = 0 try : defaults = { "vat" : "0.0", "unit" : N_("Credits"), "output" : "-", "pagesize" : "a4", \ "logo" : "/usr/share/pykota/logos/pykota.jpeg", "number" : "1", } short_options = "vho:u:V:p:l:n:" long_options = ["help", "version", "unit=", "output=", \ "pagesize=", "logo=", "vat=", "number="] # Initializes the command line tool invoiceGenerator = PKInvoice(doc=__doc__) invoiceGenerator.deferredInit() # parse and checks the command line (options, args) = invoiceGenerator.parseCommandline(sys.argv[1:], short_options, long_options, allownothing=True) # sets long options options["help"] = options["h"] or options["help"] options["version"] = options["v"] or options["version"] options["vat"] = options["V"] or options["vat"] or defaults["vat"] options["unit"] = options["u"] or options["unit"] or defaults["unit"] options["output"] = options["o"] or options["output"] or defaults["output"] options["pagesize"] = options["p"] or options["pagesize"] or defaults["pagesize"] options["number"] = options["n"] or options["number"] or defaults["number"] options["logo"] = options["l"] or options["logo"] if options["logo"] is None : # Allows --logo="" to disable the logo entirely options["logo"] = defaults["logo"] if options["help"] : invoiceGenerator.display_usage_and_quit() elif options["version"] : invoiceGenerator.display_version_and_quit() else : retcode = invoiceGenerator.main(args, options) except KeyboardInterrupt : sys.stderr.write("\nInterrupted with Ctrl+C !\n") retcode = -3 except PyKotaCommandLineError, msg : sys.stderr.write("%s : %s\n" % (sys.argv[0], msg)) retcode = -2 except SystemExit : pass except : try : invoiceGenerator.crashed("pkinvoice failed") except : crashed("pkinvoice failed") retcode = -1 try : invoiceGenerator.storage.close() except (TypeError, NameError, AttributeError) : pass sys.exit(retcode)