root / pykota / trunk / bin / pkrefund @ 3084

Revision 3084, 18.5 kB (checked in by jerome, 17 years ago)

pkrefund can now create refunding receipts in the PDF format.

  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
Line 
1#! /usr/bin/env python
2# -*- coding: ISO-8859-15 -*-
3
4"""pkrefund is a tool to refund print jobs and generate PDF receipts."""
5
6# PyKota Print Job Refund Manager
7#
8# PyKota - Print Quotas for CUPS and LPRng
9#
10# (c) 2003, 2004, 2005, 2006 Jerome Alet <alet@librelogiciel.com>
11# This program is free software; you can redistribute it and/or modify
12# it under the terms of the GNU General Public License as published by
13# the Free Software Foundation; either version 2 of the License, or
14# (at your option) any later version.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License
22# along with this program; if not, write to the Free Software
23# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
24#
25# $Id$
26#
27#
28
29import sys
30import os
31import pwd
32import time
33import cStringIO
34
35try :
36    from reportlab.pdfgen import canvas
37    from reportlab.lib import pagesizes
38    from reportlab.lib.units import cm
39except ImportError :   
40    hasRL = 0
41else :   
42    hasRL = 1
43   
44try :
45    import PIL.Image 
46except ImportError :   
47    hasPIL = 0
48else :   
49    hasPIL = 1
50
51from pykota.tool import Percent, PyKotaTool, PyKotaToolError, PyKotaCommandLineError, crashed, N_
52
53__doc__ = N_("""pkrefund v%(__version__)s (c) %(__years__)s %(__author__)s
54
55Refunds jobs.
56
57command line usage :
58
59  pkrefund [options] [filterexpr]
60
61options :
62
63  -v | --version       Prints pkrefund's version number then exits.
64  -h | --help          Prints this message then exits.
65 
66  -f | --force         Doesn't ask for confirmation before refunding jobs.
67  -r | --reason txt    Sets textual information to explain the refunding.
68
69  -l | --logo img      Use the image as the receipt's logo. The logo will
70                       be drawn at the center top of the page. The default
71                       logo is /usr/share/pykota/logos/pykota.jpeg
72
73  -p | --pagesize sz   Sets sz as the page size. Most well known
74                       page sizes are recognized, like 'A4' or 'Letter'
75                       to name a few. The default size is A4.
76
77  -n | --number N      Sets the number of the first receipt. This number
78                       will automatically be incremented for each receipt.
79
80  -o | --output f.pdf  Defines the name of the PDF file which will contain
81                       the receipts. If not set, then no PDF file will
82                       be created. If set to '-', then --force is assumed,
83                       and the PDF document is sent to standard output.
84
85  -u | --unit u        Defines the name of the unit to use on the receipts.
86                       The default unit is 'Credits', optionally translated
87                       to your native language if it is supported by PyKota.
88 
89
90  Use the filter expressions to extract only parts of the
91  datas. Allowed filters are of the form :
92               
93         key=value
94                         
95  Allowed keys for now are : 
96                       
97         username       User's name
98         printername    Printer's name
99         hostname       Client's hostname
100         jobid          Job's Id
101         billingcode    Job's billing code
102         start          Job's date of printing
103         end            Job's date of printing
104         
105  Dates formatting with 'start' and 'end' filter keys :
106 
107    YYYY : year boundaries
108    YYYYMM : month boundaries
109    YYYYMMDD : day boundaries
110    YYYYMMDDhh : hour boundaries
111    YYYYMMDDhhmm : minute boundaries
112    YYYYMMDDhhmmss : second boundaries
113    yesterday[+-NbDays] : yesterday more or less N days (e.g. : yesterday-15)
114    today[+-NbDays] : today more or less N days (e.g. : today-15)
115    tomorrow[+-NbDays] : tomorrow more or less N days (e.g. : tomorrow-15)
116    now[+-NbDays] : now more or less N days (e.g. now-15)
117
118  'now' and 'today' are not exactly the same since today represents the first
119  or last second of the day depending on if it's used in a start= or end=
120  date expression. The utility to be able to specify dates in the future is
121  a question which remains to be answered :-)
122 
123  Contrary to other PyKota management tools, wildcard characters are not
124  expanded, so you can't use them.
125 
126Examples :
127
128  $ pkrefund --output /tmp/receipts.pdf jobid=503
129 
130  This will refund all jobs which Id is 503. BEWARE : installing CUPS
131  afresh will reset the first job id at 1, so you probably want to use
132  a more precise filter as explained below. A confirmation will
133  be asked for each job to refund, and a PDF file named /tmp/receipts.pdf
134  will be created which will contain printable receipts.
135 
136  $ pkrefund --reason "Hardware problem" jobid=503 start=today-7
137 
138  Refunds all jobs which id is 503 but which were printed during the
139  past week. The reason will be marked as being an hardware problem.
140 
141  $ pkrefund --force username=jerome printername=HP2100
142 
143  Refunds all jobs printed by user jerome on printer HP2100. No
144  confirmation will be asked.
145 
146  $ pkrefund --force printername=HP2100 start=200602 end=yesterday
147 
148  Refunds all jobs printed on printer HP2100 between February 1st 2006
149  and yesterday. No confirmation will be asked.
150""")
151       
152class PkRefund(PyKotaTool) :       
153    """A class for refund manager."""
154    validfilterkeys = [ "username",
155                        "printername",
156                        "hostname",
157                        "jobid",
158                        "billingcode",
159                        "start",
160                        "end",
161                      ]
162                     
163    def getPageSize(self, pgsize) :
164        """Returns the correct page size or None if not found."""
165        try :
166            return getattr(pagesizes, pgsize.upper())
167        except AttributeError :   
168            try :
169                return getattr(pagesizes, pgsize.lower())
170            except AttributeError :
171                pass
172               
173    def printVar(self, label, value, size) :
174        """Outputs a variable onto the PDF canvas.
175       
176           Returns the number of points to substract to current Y coordinate.
177        """   
178        xcenter = (self.pagesize[0] / 2.0) - 1*cm
179        self.canvas.saveState()
180        self.canvas.setFont("Helvetica-Bold", size)
181        self.canvas.setFillColorRGB(0, 0, 0)
182        self.canvas.drawRightString(xcenter, self.ypos, "%s :" % self.userCharsetToUTF8(label))
183        self.canvas.setFont("Courier-Bold", size)
184        self.canvas.setFillColorRGB(0, 0, 1)
185        self.canvas.drawString(xcenter + 0.5*cm, self.ypos, self.userCharsetToUTF8(value))
186        self.canvas.restoreState()
187        self.ypos -= (size + 4)
188       
189    def pagePDF(self, receiptnumber, name, values, unitname, reason) :
190        """Generates a new page in the PDF document."""
191        if values["nbpages"] :
192            self.canvas.doForm("background")
193            self.ypos = self.yorigine - (cm + 20)
194            self.printVar(_("Refunding receipt"), "#%s" % receiptnumber, 22)
195            self.printVar(_("Username"), name, 22)
196            self.ypos -= 20
197            self.printVar(_("Edited on"), time.strftime("%c", time.localtime()), 14)
198               
199            self.ypos -= 20
200            self.printVar(_("Jobs refunded"), str(values["nbjobs"]), 22)
201            self.printVar(_("Pages refunded"), str(values["nbpages"]), 22)
202            self.printVar(_("Amount refunded"), "%.3f %s" % (values["nbcredits"], unitname), 22)
203            self.ypos -= 20
204            self.printVar(_("Reason"), reason, 14)
205            self.canvas.showPage()
206            return 1
207        return 0   
208       
209    def initPDF(self, logo) :
210        """Initializes the PDF document."""
211        self.pdfDocument = cStringIO.StringIO()       
212        self.canvas = c = canvas.Canvas(self.pdfDocument, \
213                                        pagesize=self.pagesize, \
214                                        pageCompression=1)
215       
216        c.setAuthor(self.originalUserName)
217        c.setTitle("PyKota print job refunding receipts")
218        c.setSubject("Print job refunding receipts generated with PyKota")
219       
220       
221        self.canvas.beginForm("background")
222        self.canvas.saveState()
223       
224        self.ypos = self.pagesize[1] - (2 * cm)           
225       
226        xcenter = self.pagesize[0] / 2.0
227        if logo :
228            try :   
229                imglogo = PIL.Image.open(logo)
230            except IOError :   
231                self.printInfo("Unable to open image %s" % logo, "warn")
232            else :
233                (width, height) = imglogo.size
234                multi = float(width) / (8 * cm) 
235                width = float(width) / multi
236                height = float(height) / multi
237                self.ypos -= height
238                c.drawImage(logo, xcenter - (width / 2.0), \
239                                  self.ypos, \
240                                  width, height)
241       
242        self.ypos -= (cm + 20)
243        self.canvas.setFont("Helvetica-Bold", 14)
244        self.canvas.setFillColorRGB(0, 0, 0)
245        msg = _("Here's the receipt for the refunding of your print jobs")
246        self.canvas.drawCentredString(xcenter, self.ypos, "%s :" % self.userCharsetToUTF8(msg))
247       
248        self.yorigine = self.ypos
249        self.canvas.restoreState()
250        self.canvas.endForm()
251       
252    def endPDF(self, fname) :   
253        """Flushes the PDF generator."""
254        self.canvas.save()
255        if fname != "-" :       
256            outfile = open(fname, "w")
257            outfile.write(self.pdfDocument.getvalue())
258            outfile.close()
259        else :   
260            sys.stdout.write(self.pdfDocument.getvalue())
261            sys.stdout.flush()
262       
263    def genReceipts(self, peruser, logo, outfname, firstnumber, reason) :
264        """Generates the receipts file."""
265        if outfname and len(peruser) :
266            percent = Percent(self)
267            percent.setSize(len(peruser))
268            if outfname != "-" :
269                percent.display("%s...\n" % _("Generating receipts"))
270               
271            self.initPDF(logo)
272            number = firstnumber
273            for (name, values) in peruser.items() :
274                number += self.pagePDF(number, name, values, options["unit"], reason)
275                if outfname != "-" :
276                    percent.oneMore()
277                   
278            if number > firstnumber :
279                self.endPDF(outfname)
280               
281            if outfname != "-" :
282                percent.done()
283       
284    def main(self, arguments, options, restricted=1) :
285        """Print Quota Data Dumper."""
286        if not hasRL :
287            raise PyKotaToolError, "The ReportLab module is missing. Download it from http://www.reportlab.org"
288        if not hasPIL :
289            raise PyKotaToolError, "The Python Imaging Library is missing. Download it from http://www.pythonware.com/downloads"
290           
291        if restricted and not self.config.isAdmin :
292            raise PyKotaCommandLineError, "%s : %s" % (pwd.getpwuid(os.geteuid())[0], _("You're not allowed to use this command."))
293           
294        if (not options["reason"]) or not options["reason"].strip() :
295            raise PyKotaCommandLineError, _("Refunding for no reason is forbidden. Please use the --reason command line option.")
296           
297        if options["output"] :
298            options["output"] = options["output"].strip()
299            if options["output"] == "-" :
300                options["force"] = True
301                self.printInfo(_("The PDF file containing the receipts will be sent to stdout. --force is assumed."), "warn")
302           
303        try :   
304            firstnumber = int(options["number"])
305            if firstnumber <= 0 :
306                raise ValueError
307        except (ValueError, TypeError) :   
308            raise PyKotaCommandLineError, _("Incorrect value '%s' for the --number command line option") % options["number"]
309           
310        self.pagesize = self.getPageSize(options["pagesize"])
311        if self.pagesize is None :
312            self.pagesize = self.getPageSize("a4")
313            self.printInfo(_("Invalid 'pagesize' option %s, defaulting to A4.") % options["pagesize"], "warn")
314           
315        extractonly = {}
316        for filterexp in arguments :
317            if filterexp.strip() :
318                try :
319                    (filterkey, filtervalue) = [part.strip() for part in filterexp.split("=")]
320                    filterkey = filterkey.lower()
321                    if filterkey not in self.validfilterkeys :
322                        raise ValueError               
323                except ValueError :   
324                    raise PyKotaCommandLineError, _("Invalid filter value [%s], see help.") % filterexp
325                else :   
326                    extractonly.update({ filterkey : filtervalue })
327           
328        username = extractonly.get("username")   
329        if username :
330            user = self.storage.getUser(username)
331        else :
332            user = None
333           
334        printername = extractonly.get("printername")   
335        if printername :
336            printer = self.storage.getPrinter(printername)
337        else :   
338            printer = None
339           
340        start = extractonly.get("start")
341        end = extractonly.get("end")
342        (start, end) = self.storage.cleanDates(start, end)
343       
344        jobs = self.storage.retrieveHistory(user=user,   
345                                            printer=printer, 
346                                            hostname=extractonly.get("hostname"),
347                                            billingcode=extractonly.get("billingcode"),
348                                            jobid=extractonly.get("jobid"),
349                                            start=start,
350                                            end=end,
351                                            limit=0)
352        peruser = {}                                   
353        nbjobs = 0                                   
354        nbpages = 0                                           
355        nbcredits = 0.0
356        reason = (options.get("reason") or "").strip()
357        for job in jobs :                                   
358            if job.JobSize and (job.JobAction not in ("DENY", "CANCEL", "REFUND")) :
359                if options["force"] :
360                    nbpages += job.JobSize
361                    nbcredits += job.JobPrice
362                    counters = peruser.setdefault(job.UserName, { "nbjobs" : 0, "nbpages" : 0, "nbcredits" : 0.0 })
363                    counters["nbpages"] += job.JobSize
364                    counters["nbcredits"] += job.JobPrice
365                    job.refund(reason)
366                    counters["nbjobs"] += 1
367                    nbjobs += 1
368                else :   
369                    print _("Date : %s") % str(job.JobDate)[:19]
370                    print _("JobId : %s") % job.JobId
371                    print _("Username : %s") % job.UserName
372                    print _("Printername : %s") % job.PrinterName
373                    print _("Billing code : %s") % job.JobBillingCode
374                    print _("Pages : %i") % job.JobSize
375                    print _("Credits : %.3f") % job.JobPrice
376                    print _("Title : %s") % job.JobTitle
377                   
378                    while True :                             
379                        answer = raw_input("\t%s ? " % _("Refund (Y/N)")).strip().upper()
380                        if answer == _("Y") :
381                            nbpages += job.JobSize
382                            nbcredits += job.JobPrice
383                            counters = peruser.setdefault(job.UserName, { "nbjobs" : 0, "nbpages" : 0, "nbcredits" : 0.0 })
384                            counters["nbpages"] += job.JobSize
385                            counters["nbcredits"] += job.JobPrice
386                            job.refund(reason)
387                            counters["nbjobs"] += 1
388                            nbjobs += 1
389                            break   
390                        elif answer == _("N") :   
391                            break
392                    print       
393        self.genReceipts(peruser, options["logo"].strip(), options["output"], firstnumber, reason)
394        if options["output"] != "-" :   
395            print _("Refunded %i jobs, %i pages and %.3f credits") % (nbjobs, nbpages, nbcredits)
396           
397if __name__ == "__main__" : 
398    retcode = 0
399    try :
400        defaults = { "unit" : N_("Credits"),
401                     "pagesize" : "a4", \
402                     "logo" : "/usr/share/pykota/logos/pykota.jpeg",
403                     "number" : "1",
404                   }
405        short_options = "vhfru:o:p:l:n:"
406        long_options = ["help", "version", "force", "reason=", "unit=", "output=", "pagesize=", "logo=", "number="]
407       
408        # Initializes the command line tool
409        refundmanager = PkRefund(doc=__doc__)
410        refundmanager.deferredInit()
411       
412        # parse and checks the command line
413        (options, args) = refundmanager.parseCommandline(sys.argv[1:], short_options, long_options, allownothing=1)
414       
415        # sets long options
416        options["help"] = options["h"] or options["help"]
417        options["version"] = options["v"] or options["version"]
418        options["force"] = options["f"] or options["force"]
419        options["reason"] = options["r"] or options["reason"]
420        options["unit"] = options["u"] or options["unit"] or defaults["unit"]
421        options["output"] = options["o"] or options["output"]
422        options["pagesize"] = options["p"] or options["pagesize"] or defaults["pagesize"]
423        options["number"] = options["n"] or options["number"] or defaults["number"]
424        options["logo"] = options["l"] or options["logo"]
425        if options["logo"] is None : # Allows --logo="" to disable the logo entirely
426            options["logo"] = defaults["logo"] 
427       
428        if options["help"] :
429            refundmanager.display_usage_and_quit()
430        elif options["version"] :
431            refundmanager.display_version_and_quit()
432        else :
433            retcode = refundmanager.main(args, options)
434    except KeyboardInterrupt :       
435        sys.stderr.write("\nInterrupted with Ctrl+C !\n")
436        retcode = -3
437    except PyKotaCommandLineError, msg :   
438        sys.stderr.write("%s : %s\n" % (sys.argv[0], msg))
439        retcode = -2
440    except SystemExit :       
441        pass
442    except :
443        try :
444            refundmanager.crashed("pkrefund failed")
445        except :   
446            crashed("pkrefund failed")
447        retcode = -1
448
449    try :
450        refundmanager.storage.close()
451    except (TypeError, NameError, AttributeError) :   
452        pass
453       
454    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.