root / pykota / trunk / bin / pkrefund @ 3294

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

Added modules to store utility functions and application
intialization code, which has nothing to do in classes.
Modified tool.py accordingly (far from being finished)
Use these new modules where necessary.
Now converts all command line arguments to unicode before
beginning to work. Added a proper logging method for already
encoded query strings.

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