root / pykota / trunk / bin / pkrefund @ 3260

Revision 3260, 18.7 kB (checked in by jerome, 16 years ago)

Changed license to GNU GPL v3 or later.
Changed Python source encoding from ISO-8859-15 to UTF-8 (only ASCII
was used anyway).

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