root / pykota / trunk / bin / pkrefund @ 3108

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

Better textual information.

  • 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"]), 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 _("JobId : %s") % job.JobId
379                    print _("User : %s") % job.UserName
380                    print _("Printer : %s") % job.PrinterName
381                    print _("Billing code : %s") % job.JobBillingCode
382                    print _("Pages : %i") % job.JobSize
383                    print _("Credits : %.3f") % job.JobPrice
384                    print _("Title : %s") % job.JobTitle
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        sys.stderr.write("\nInterrupted with Ctrl+C !\n")
446        retcode = -3
447    except PyKotaCommandLineError, msg :   
448        sys.stderr.write("%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.