root / pykota / trunk / bin / pkrefund @ 3288

Revision 3288, 18.8 kB (checked in by jerome, 16 years ago)

Moved all exceptions definitions to a dedicated module.

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