#! /usr/bin/env python # -*- coding: ISO-8859-15 -*- """pkrefund is a tool to refund print jobs and generate PDF receipts.""" # PyKota Print Job Refund Manager # # 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_("""pkrefund v%(__version__)s (c) %(__years__)s %(__author__)s Refunds jobs. command line usage : pkrefund [options] [filterexpr] options : -v | --version Prints pkrefund's version number then exits. -h | --help Prints this message then exits. -f | --force Doesn't ask for confirmation before refunding jobs. -r | --reason txt Sets textual information to explain the refunding. -l | --logo img Use the image as the receipt'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 receipt. This number will automatically be incremented for each receipt. -o | --output f.pdf Defines the name of the PDF file which will contain the receipts. If not set, then no PDF file will be created. If set to '-', then --force is assumed, and the PDF document is sent to standard output. -u | --unit u Defines the name of the unit to use on the receipts. The default unit is 'Credits', optionally translated to your native language if it is supported by PyKota. 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 : $ pkrefund --output /tmp/receipts.pdf jobid=503 This will refund all jobs which Id is 503. BEWARE : installing CUPS afresh will reset the first job id at 1, so you probably want to use a more precise filter as explained below. A confirmation will be asked for each job to refund, and a PDF file named /tmp/receipts.pdf will be created which will contain printable receipts. $ pkrefund --reason "Hardware problem" jobid=503 start=today-7 Refunds all jobs which id is 503 but which were printed during the past week. The reason will be marked as being an hardware problem. $ pkrefund --force username=jerome printername=HP2100 Refunds all jobs printed by user jerome on printer HP2100. No confirmation will be asked. $ pkrefund --force printername=HP2100 start=200602 end=yesterday Refunds all jobs printed on printer HP2100 between February 1st 2006 and yesterday. No confirmation will be asked. """) class PkRefund(PyKotaTool) : """A class for refund manager.""" 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, receiptnumber, name, values, unit, reason) : """Generates a new page in the PDF document.""" if values["nbpages"] : self.canvas.doForm("background") self.ypos = self.yorigine - (cm + 20) self.printVar(_("Refunding receipt"), "#%s" % receiptnumber, 22) self.printVar(_("Username"), name, 22) self.ypos -= 20 self.printVar(_("Edited on"), time.strftime("%c", time.localtime()), 14) self.ypos -= 20 self.printVar(_("Jobs refunded"), str(values["nbjobs"]), 18) self.printVar(_("Pages refunded"), str(values["nbpages"]), 18) self.printVar(_("Amount refunded"), "%.3f %s" % (values["nbcredits"], unit), 18) self.ypos -= 20 self.printVar(_("Reason"), reason, 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 print job refunding receipts") c.setSubject("Print job refunding receipts 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 receipt for the refunding of your print jobs") 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 genReceipts(self, peruser, logo, outfname, firstnumber, reason, unit) : """Generates the receipts file.""" if outfname and len(peruser) : percent = Percent(self, size=len(peruser)) if outfname != "-" : percent.display("%s...\n" % _("Generating receipts")) self.initPDF(logo) number = firstnumber for (name, values) in peruser.items() : number += self.pagePDF(number, name, values, unit, reason) if outfname != "-" : percent.oneMore() if number > firstnumber : self.endPDF(outfname) if outfname != "-" : percent.done() def main(self, arguments, options, restricted=1) : """Print Quota Data Dumper.""" 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 restricted and not self.config.isAdmin : raise PyKotaCommandLineError, "%s : %s" % (pwd.getpwuid(os.geteuid())[0], _("You're not allowed to use this command.")) if (not options["reason"]) or not options["reason"].strip() : raise PyKotaCommandLineError, _("Refunding for no reason is forbidden. Please use the --reason command line option.") outfname = options["output"] if outfname : outfname = outfname.strip() if outfname == "-" : options["force"] = True self.printInfo(_("The PDF file containing the receipts will be sent to stdout. --force is assumed."), "warn") try : firstnumber = int(options["number"]) if firstnumber <= 0 : raise ValueError except (ValueError, TypeError) : 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) 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 reason = (options.get("reason") or "").strip() percent.setSize(len(jobs)) if outfname != "-" : percent.display("\n") for job in jobs : if job.JobSize and (job.JobAction not in ("DENY", "CANCEL", "REFUND")) : if options["force"] : 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 job.refund(reason) counters["nbjobs"] += 1 nbjobs += 1 if outfname != "-" : percent.oneMore() else : print _("Date : %s") % str(job.JobDate)[:19] print _("Printer : %s") % job.PrinterName print _("User : %s") % job.UserName print _("JobId : %s") % job.JobId print _("Title : %s") % job.JobTitle if job.JobBillingCode : print _("Billing code : %s") % job.JobBillingCode print _("Pages : %i") % job.JobSize print _("Credits : %.3f") % job.JobPrice while True : answer = raw_input("\t%s ? " % _("Refund (Y/N)")).strip().upper() if answer == _("Y") : 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 job.refund(reason) counters["nbjobs"] += 1 nbjobs += 1 break elif answer == _("N") : break print if outfname != "-" : percent.done() self.genReceipts(peruser, options["logo"].strip(), outfname, firstnumber, reason, options["unit"]) if outfname != "-" : print _("Refunded %i users for %i jobs, %i pages and %.3f credits") % (len(peruser), nbjobs, nbpages, nbcredits) if __name__ == "__main__" : retcode = 0 try : defaults = { "unit" : N_("Credits"), "pagesize" : "a4", \ "logo" : "/usr/share/pykota/logos/pykota.jpeg", "number" : "1", } short_options = "vhfru:o:p:l:n:" long_options = ["help", "version", "force", "reason=", "unit=", "output=", "pagesize=", "logo=", "number="] # Initializes the command line tool refundmanager = PkRefund(doc=__doc__) refundmanager.deferredInit() # parse and checks the command line (options, args) = refundmanager.parseCommandline(sys.argv[1:], short_options, long_options, allownothing=1) # sets long options options["help"] = options["h"] or options["help"] options["version"] = options["v"] or options["version"] options["force"] = options["f"] or options["force"] options["reason"] = options["r"] or options["reason"] options["unit"] = options["u"] or options["unit"] or defaults["unit"] options["output"] = options["o"] or options["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"] : refundmanager.display_usage_and_quit() elif options["version"] : refundmanager.display_version_and_quit() else : retcode = refundmanager.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 : refundmanager.crashed("pkrefund failed") except : crashed("pkrefund failed") retcode = -1 try : refundmanager.storage.close() except (TypeError, NameError, AttributeError) : pass sys.exit(retcode)