root / pykota / trunk / bin / pkrefund @ 3133

Revision 3133, 18.9 kB (checked in by jerome, 17 years ago)

Changed copyright years.

  • 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, 2007 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"]), 18)
201            self.printVar(_("Pages refunded"), str(values["nbpages"]), 18)
202            self.printVar(_("Amount refunded"), "%.3f %s" % (values["nbcredits"], unit), 18)
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        outfname = options["output"]
296        if outfname :
297            outfname = outfname.strip()
298            if outfname == "-" :
299                options["force"] = True
300                self.printInfo(_("The PDF file containing the receipts will be sent to stdout. --force is assumed."), "warn")
301           
302        try :   
303            firstnumber = int(options["number"])
304            if firstnumber <= 0 :
305                raise ValueError
306        except (ValueError, TypeError) :   
307            raise PyKotaCommandLineError, _("Incorrect value '%s' for the --number command line option") % options["number"]
308           
309        self.pagesize = self.getPageSize(options["pagesize"])
310        if self.pagesize is None :
311            self.pagesize = self.getPageSize("a4")
312            self.printInfo(_("Invalid 'pagesize' option %s, defaulting to A4.") % options["pagesize"], "warn")
313           
314        extractonly = {}
315        for filterexp in arguments :
316            if filterexp.strip() :
317                try :
318                    (filterkey, filtervalue) = [part.strip() for part in filterexp.split("=")]
319                    filterkey = filterkey.lower()
320                    if filterkey not in self.validfilterkeys :
321                        raise ValueError               
322                except ValueError :   
323                    raise PyKotaCommandLineError, _("Invalid filter value [%s], see help.") % filterexp
324                else :   
325                    extractonly.update({ filterkey : filtervalue })
326           
327        percent = Percent(self)
328        if outfname != "-" :
329            percent.display("%s..." % _("Extracting datas"))
330           
331        username = extractonly.get("username")   
332        if username :
333            user = self.storage.getUser(username)
334        else :
335            user = None
336           
337        printername = extractonly.get("printername")   
338        if printername :
339            printer = self.storage.getPrinter(printername)
340        else :   
341            printer = None
342           
343        start = extractonly.get("start")
344        end = extractonly.get("end")
345        (start, end) = self.storage.cleanDates(start, end)
346       
347        jobs = self.storage.retrieveHistory(user=user,   
348                                            printer=printer, 
349                                            hostname=extractonly.get("hostname"),
350                                            billingcode=extractonly.get("billingcode"),
351                                            jobid=extractonly.get("jobid"),
352                                            start=start,
353                                            end=end,
354                                            limit=0)
355        peruser = {}                                   
356        nbjobs = 0                                   
357        nbpages = 0                                           
358        nbcredits = 0.0
359        reason = (options.get("reason") or "").strip()
360        percent.setSize(len(jobs))
361        if outfname != "-" :
362            percent.display("\n")
363        for job in jobs :                                   
364            if job.JobSize and (job.JobAction not in ("DENY", "CANCEL", "REFUND")) :
365                if options["force"] :
366                    nbpages += job.JobSize
367                    nbcredits += job.JobPrice
368                    counters = peruser.setdefault(job.UserName, { "nbjobs" : 0, "nbpages" : 0, "nbcredits" : 0.0 })
369                    counters["nbpages"] += job.JobSize
370                    counters["nbcredits"] += job.JobPrice
371                    job.refund(reason)
372                    counters["nbjobs"] += 1
373                    nbjobs += 1
374                    if outfname != "-" :
375                        percent.oneMore()
376                else :   
377                    print _("Date : %s") % str(job.JobDate)[:19]
378                    print _("Printer : %s") % job.PrinterName
379                    print _("User : %s") % job.UserName
380                    print _("JobId : %s") % job.JobId
381                    print _("Title : %s") % job.JobTitle
382                    if job.JobBillingCode :
383                        print _("Billing code : %s") % job.JobBillingCode
384                    print _("Pages : %i") % job.JobSize
385                    print _("Credits : %.3f") % job.JobPrice
386                   
387                    while True :                             
388                        answer = raw_input("\t%s ? " % _("Refund (Y/N)")).strip().upper()
389                        if answer == _("Y") :
390                            nbpages += job.JobSize
391                            nbcredits += job.JobPrice
392                            counters = peruser.setdefault(job.UserName, { "nbjobs" : 0, "nbpages" : 0, "nbcredits" : 0.0 })
393                            counters["nbpages"] += job.JobSize
394                            counters["nbcredits"] += job.JobPrice
395                            job.refund(reason)
396                            counters["nbjobs"] += 1
397                            nbjobs += 1
398                            break   
399                        elif answer == _("N") :   
400                            break
401                    print       
402        if outfname != "-" :
403            percent.done()
404        self.genReceipts(peruser, options["logo"].strip(), outfname, firstnumber, reason, options["unit"])
405        if outfname != "-" :   
406            print _("Refunded %i users for %i jobs, %i pages and %.3f credits") % (len(peruser), nbjobs, nbpages, nbcredits)
407           
408if __name__ == "__main__" : 
409    retcode = 0
410    try :
411        defaults = { "unit" : N_("Credits"),
412                     "pagesize" : "a4", \
413                     "logo" : "/usr/share/pykota/logos/pykota.jpeg",
414                     "number" : "1",
415                   }
416        short_options = "vhfru:o:p:l:n:"
417        long_options = ["help", "version", "force", "reason=", "unit=", "output=", "pagesize=", "logo=", "number="]
418       
419        # Initializes the command line tool
420        refundmanager = PkRefund(doc=__doc__)
421        refundmanager.deferredInit()
422       
423        # parse and checks the command line
424        (options, args) = refundmanager.parseCommandline(sys.argv[1:], short_options, long_options, allownothing=1)
425       
426        # sets long options
427        options["help"] = options["h"] or options["help"]
428        options["version"] = options["v"] or options["version"]
429        options["force"] = options["f"] or options["force"]
430        options["reason"] = options["r"] or options["reason"]
431        options["unit"] = options["u"] or options["unit"] or defaults["unit"]
432        options["output"] = options["o"] or options["output"]
433        options["pagesize"] = options["p"] or options["pagesize"] or defaults["pagesize"]
434        options["number"] = options["n"] or options["number"] or defaults["number"]
435        options["logo"] = options["l"] or options["logo"]
436        if options["logo"] is None : # Allows --logo="" to disable the logo entirely
437            options["logo"] = defaults["logo"] 
438       
439        if options["help"] :
440            refundmanager.display_usage_and_quit()
441        elif options["version"] :
442            refundmanager.display_version_and_quit()
443        else :
444            retcode = refundmanager.main(args, options)
445    except KeyboardInterrupt :       
446        sys.stderr.write("\nInterrupted with Ctrl+C !\n")
447        retcode = -3
448    except PyKotaCommandLineError, msg :   
449        sys.stderr.write("%s : %s\n" % (sys.argv[0], msg))
450        retcode = -2
451    except SystemExit :       
452        pass
453    except :
454        try :
455            refundmanager.crashed("pkrefund failed")
456        except :   
457            crashed("pkrefund failed")
458        retcode = -1
459
460    try :
461        refundmanager.storage.close()
462    except (TypeError, NameError, AttributeError) :   
463        pass
464       
465    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.