root / pykota / trunk / bin / pkrefund @ 3088

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

Minor improvements.

  • 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, unit, 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"], unit), 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        self.canvas.beginForm("background")
221        self.canvas.saveState()
222       
223        self.ypos = self.pagesize[1] - (2 * cm)           
224       
225        xcenter = self.pagesize[0] / 2.0
226        if logo :
227            try :   
228                imglogo = PIL.Image.open(logo)
229            except IOError :   
230                self.printInfo("Unable to open image %s" % logo, "warn")
231            else :
232                (width, height) = imglogo.size
233                multi = float(width) / (8 * cm) 
234                width = float(width) / multi
235                height = float(height) / multi
236                self.ypos -= height
237                c.drawImage(logo, xcenter - (width / 2.0), \
238                                  self.ypos, \
239                                  width, height)
240       
241        self.ypos -= (cm + 20)
242        self.canvas.setFont("Helvetica-Bold", 14)
243        self.canvas.setFillColorRGB(0, 0, 0)
244        msg = _("Here's the receipt for the refunding of your print jobs")
245        self.canvas.drawCentredString(xcenter, self.ypos, "%s :" % self.userCharsetToUTF8(msg))
246       
247        self.yorigine = self.ypos
248        self.canvas.restoreState()
249        self.canvas.endForm()
250       
251    def endPDF(self, fname) :   
252        """Flushes the PDF generator."""
253        self.canvas.save()
254        if fname != "-" :       
255            outfile = open(fname, "w")
256            outfile.write(self.pdfDocument.getvalue())
257            outfile.close()
258        else :   
259            sys.stdout.write(self.pdfDocument.getvalue())
260            sys.stdout.flush()
261       
262    def genReceipts(self, peruser, logo, outfname, firstnumber, reason, unit) :
263        """Generates the receipts file."""
264        if outfname and len(peruser) :
265            percent = Percent(self, size=len(peruser))
266            if outfname != "-" :
267                percent.display("%s...\n" % _("Generating receipts"))
268               
269            self.initPDF(logo)
270            number = firstnumber
271            for (name, values) in peruser.items() :
272                number += self.pagePDF(number, name, values, unit, reason)
273                if outfname != "-" :
274                    percent.oneMore()
275                   
276            if number > firstnumber :
277                self.endPDF(outfname)
278               
279            if outfname != "-" :
280                percent.done()
281       
282    def main(self, arguments, options, restricted=1) :
283        """Print Quota Data Dumper."""
284        if not hasRL :
285            raise PyKotaToolError, "The ReportLab module is missing. Download it from http://www.reportlab.org"
286        if not hasPIL :
287            raise PyKotaToolError, "The Python Imaging Library is missing. Download it from http://www.pythonware.com/downloads"
288           
289        if restricted and not self.config.isAdmin :
290            raise PyKotaCommandLineError, "%s : %s" % (pwd.getpwuid(os.geteuid())[0], _("You're not allowed to use this command."))
291           
292        if (not options["reason"]) or not options["reason"].strip() :
293            raise PyKotaCommandLineError, _("Refunding for no reason is forbidden. Please use the --reason command line option.")
294           
295        if options["output"] :
296            options["output"] = options["output"].strip()
297            if options["output"] == "-" :
298                options["force"] = True
299                self.printInfo(_("The PDF file containing the receipts will be sent to stdout. --force is assumed."), "warn")
300           
301        try :   
302            firstnumber = int(options["number"])
303            if firstnumber <= 0 :
304                raise ValueError
305        except (ValueError, TypeError) :   
306            raise PyKotaCommandLineError, _("Incorrect value '%s' for the --number command line option") % options["number"]
307           
308        self.pagesize = self.getPageSize(options["pagesize"])
309        if self.pagesize is None :
310            self.pagesize = self.getPageSize("a4")
311            self.printInfo(_("Invalid 'pagesize' option %s, defaulting to A4.") % options["pagesize"], "warn")
312           
313        extractonly = {}
314        for filterexp in arguments :
315            if filterexp.strip() :
316                try :
317                    (filterkey, filtervalue) = [part.strip() for part in filterexp.split("=")]
318                    filterkey = filterkey.lower()
319                    if filterkey not in self.validfilterkeys :
320                        raise ValueError               
321                except ValueError :   
322                    raise PyKotaCommandLineError, _("Invalid filter value [%s], see help.") % filterexp
323                else :   
324                    extractonly.update({ filterkey : filtervalue })
325           
326        username = extractonly.get("username")   
327        if username :
328            user = self.storage.getUser(username)
329        else :
330            user = None
331           
332        printername = extractonly.get("printername")   
333        if printername :
334            printer = self.storage.getPrinter(printername)
335        else :   
336            printer = None
337           
338        start = extractonly.get("start")
339        end = extractonly.get("end")
340        (start, end) = self.storage.cleanDates(start, end)
341       
342        jobs = self.storage.retrieveHistory(user=user,   
343                                            printer=printer, 
344                                            hostname=extractonly.get("hostname"),
345                                            billingcode=extractonly.get("billingcode"),
346                                            jobid=extractonly.get("jobid"),
347                                            start=start,
348                                            end=end,
349                                            limit=0)
350        peruser = {}                                   
351        nbjobs = 0                                   
352        nbpages = 0                                           
353        nbcredits = 0.0
354        reason = (options.get("reason") or "").strip()
355        for job in jobs :                                   
356            if job.JobSize and (job.JobAction not in ("DENY", "CANCEL", "REFUND")) :
357                if options["force"] :
358                    nbpages += job.JobSize
359                    nbcredits += job.JobPrice
360                    counters = peruser.setdefault(job.UserName, { "nbjobs" : 0, "nbpages" : 0, "nbcredits" : 0.0 })
361                    counters["nbpages"] += job.JobSize
362                    counters["nbcredits"] += job.JobPrice
363                    job.refund(reason)
364                    counters["nbjobs"] += 1
365                    nbjobs += 1
366                else :   
367                    print _("Date : %s") % str(job.JobDate)[:19]
368                    print _("JobId : %s") % job.JobId
369                    print _("Username : %s") % job.UserName
370                    print _("Printername : %s") % job.PrinterName
371                    print _("Billing code : %s") % job.JobBillingCode
372                    print _("Pages : %i") % job.JobSize
373                    print _("Credits : %.3f") % job.JobPrice
374                    print _("Title : %s") % job.JobTitle
375                   
376                    while True :                             
377                        answer = raw_input("\t%s ? " % _("Refund (Y/N)")).strip().upper()
378                        if answer == _("Y") :
379                            nbpages += job.JobSize
380                            nbcredits += job.JobPrice
381                            counters = peruser.setdefault(job.UserName, { "nbjobs" : 0, "nbpages" : 0, "nbcredits" : 0.0 })
382                            counters["nbpages"] += job.JobSize
383                            counters["nbcredits"] += job.JobPrice
384                            job.refund(reason)
385                            counters["nbjobs"] += 1
386                            nbjobs += 1
387                            break   
388                        elif answer == _("N") :   
389                            break
390                    print       
391        self.genReceipts(peruser, options["logo"].strip(), options["output"], firstnumber, reason, options["unit"])
392        if options["output"] != "-" :   
393            print _("Refunded %i users for %i jobs, %i pages and %.3f credits") % (len(peruser), nbjobs, nbpages, nbcredits)
394           
395if __name__ == "__main__" : 
396    retcode = 0
397    try :
398        defaults = { "unit" : N_("Credits"),
399                     "pagesize" : "a4", \
400                     "logo" : "/usr/share/pykota/logos/pykota.jpeg",
401                     "number" : "1",
402                   }
403        short_options = "vhfru:o:p:l:n:"
404        long_options = ["help", "version", "force", "reason=", "unit=", "output=", "pagesize=", "logo=", "number="]
405       
406        # Initializes the command line tool
407        refundmanager = PkRefund(doc=__doc__)
408        refundmanager.deferredInit()
409       
410        # parse and checks the command line
411        (options, args) = refundmanager.parseCommandline(sys.argv[1:], short_options, long_options, allownothing=1)
412       
413        # sets long options
414        options["help"] = options["h"] or options["help"]
415        options["version"] = options["v"] or options["version"]
416        options["force"] = options["f"] or options["force"]
417        options["reason"] = options["r"] or options["reason"]
418        options["unit"] = options["u"] or options["unit"] or defaults["unit"]
419        options["output"] = options["o"] or options["output"]
420        options["pagesize"] = options["p"] or options["pagesize"] or defaults["pagesize"]
421        options["number"] = options["n"] or options["number"] or defaults["number"]
422        options["logo"] = options["l"] or options["logo"]
423        if options["logo"] is None : # Allows --logo="" to disable the logo entirely
424            options["logo"] = defaults["logo"] 
425       
426        if options["help"] :
427            refundmanager.display_usage_and_quit()
428        elif options["version"] :
429            refundmanager.display_version_and_quit()
430        else :
431            retcode = refundmanager.main(args, options)
432    except KeyboardInterrupt :       
433        sys.stderr.write("\nInterrupted with Ctrl+C !\n")
434        retcode = -3
435    except PyKotaCommandLineError, msg :   
436        sys.stderr.write("%s : %s\n" % (sys.argv[0], msg))
437        retcode = -2
438    except SystemExit :       
439        pass
440    except :
441        try :
442            refundmanager.crashed("pkrefund failed")
443        except :   
444            crashed("pkrefund failed")
445        retcode = -1
446
447    try :
448        refundmanager.storage.close()
449    except (TypeError, NameError, AttributeError) :   
450        pass
451       
452    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.