root / pykota / trunk / bin / pkrefund @ 3434

Revision 3434, 15.6 kB (checked in by jerome, 16 years ago)

Moved the progress report code to its own 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
24"""A refunding tool for PyKota"""
25
26import sys
27import os
28import pwd
29import time
30import cStringIO
31
32try :
33    from reportlab.pdfgen import canvas
34    from reportlab.lib import pagesizes
35    from reportlab.lib.units import cm
36except ImportError :
37    hasRL = False
38else :
39    hasRL = True
40
41try :
42    import PIL.Image
43except ImportError :
44    hasPIL = False
45else :
46    hasPIL = True
47
48import pykota.appinit
49from pykota.utils import run
50from pykota.commandline import PyKotaOptionParser, \
51                               checkandset_pagesize, \
52                               checkandset_positiveint
53from pykota.pdfutils import getPageSize
54from pykota.errors import PyKotaToolError, PyKotaCommandLineError
55from pykota.tool import PyKotaTool
56from pykota.progressbar import Percent
57
58class PKRefund(PyKotaTool) :
59    """A class for refund manager."""
60    validfilterkeys = [ "username",
61                        "printername",
62                        "hostname",
63                        "jobid",
64                        "billingcode",
65                        "start",
66                        "end",
67                      ]
68
69    def printVar(self, label, value, size) :
70        """Outputs a variable onto the PDF canvas.
71
72           Returns the number of points to substract to current Y coordinate.
73        """
74        xcenter = (self.pagesize[0] / 2.0) - 1*cm
75        self.canvas.saveState()
76        self.canvas.setFont("Helvetica-Bold", size)
77        self.canvas.setFillColorRGB(0, 0, 0)
78        self.canvas.drawRightString(xcenter, self.ypos, "%s :" % label)
79        self.canvas.setFont("Courier-Bold", size)
80        self.canvas.setFillColorRGB(0, 0, 1)
81        self.canvas.drawString(xcenter + 0.5*cm, self.ypos, value)
82        self.canvas.restoreState()
83        self.ypos -= (size + 4)
84
85    def pagePDF(self, receiptnumber, name, values, unit, reason) :
86        """Generates a new page in the PDF document."""
87        if values["nbpages"] :
88            self.canvas.doForm("background")
89            self.ypos = self.yorigine - (cm + 20)
90            self.printVar(_("Refunding receipt"), "#%s" % receiptnumber, 22)
91            self.printVar(_("Username"), name, 22)
92            self.ypos -= 20
93            datetime = time.strftime("%c", time.localtime()).decode(self.charset, "replace")
94            self.printVar(_("Edited on"), datetime, 14)
95
96            self.ypos -= 20
97            self.printVar(_("Jobs refunded"), str(values["nbjobs"]), 18)
98            self.printVar(_("Pages refunded"), str(values["nbpages"]), 18)
99            self.printVar(_("Amount refunded"), "%.3f %s" % (values["nbcredits"], unit), 18)
100            self.ypos -= 20
101            self.printVar(_("Reason"), reason, 14)
102            self.canvas.showPage()
103            return 1
104        return 0
105
106    def initPDF(self, logo) :
107        """Initializes the PDF document."""
108        self.pdfDocument = cStringIO.StringIO()
109        self.canvas = c = canvas.Canvas(self.pdfDocument, \
110                                        pagesize=self.pagesize, \
111                                        pageCompression=1)
112
113        c.setAuthor(self.effectiveUserName)
114        c.setTitle(_("PyKota print job refunding receipts"))
115        c.setSubject(_("Print job refunding receipts generated with PyKota"))
116
117        self.canvas.beginForm("background")
118        self.canvas.saveState()
119
120        self.ypos = self.pagesize[1] - (2 * cm)
121
122        xcenter = self.pagesize[0] / 2.0
123        if logo :
124            try :
125                imglogo = PIL.Image.open(logo)
126            except IOError :
127                self.printInfo("Unable to open image %s" % logo, "warn")
128            else :
129                (width, height) = imglogo.size
130                multi = float(width) / (8 * cm)
131                width = float(width) / multi
132                height = float(height) / multi
133                self.ypos -= height
134                c.drawImage(logo, xcenter - (width / 2.0), \
135                                  self.ypos, \
136                                  width, height)
137
138        self.ypos -= (cm + 20)
139        self.canvas.setFont("Helvetica-Bold", 14)
140        self.canvas.setFillColorRGB(0, 0, 0)
141        msg = _("Here's the receipt for the refunding of your print jobs")
142        self.canvas.drawCentredString(xcenter, self.ypos, "%s :" % msg)
143
144        self.yorigine = self.ypos
145        self.canvas.restoreState()
146        self.canvas.endForm()
147
148    def endPDF(self, fname) :
149        """Flushes the PDF generator."""
150        self.canvas.save()
151        if fname != "-" :
152            outfile = open(fname, "w")
153            outfile.write(self.pdfDocument.getvalue())
154            outfile.close()
155        else :
156            sys.stdout.write(self.pdfDocument.getvalue())
157            sys.stdout.flush()
158
159    def genReceipts(self, peruser, logo, outfname, firstnumber, reason, unit) :
160        """Generates the receipts file."""
161        if len(peruser) :
162            percent = Percent(self, size=len(peruser))
163            if outfname != "-" :
164                percent.display("%s...\n" % _("Generating receipts"))
165
166            self.initPDF(logo)
167            number = firstnumber
168            for (name, values) in peruser.items() :
169                number += self.pagePDF(number, name, values, unit, reason)
170                if outfname != "-" :
171                    percent.oneMore()
172
173            if number > firstnumber :
174                self.endPDF(outfname)
175
176            if outfname != "-" :
177                percent.done()
178
179    def main(self, arguments, options) :
180        """Refunds jobs."""
181        if not hasRL :
182            raise PyKotaToolError, "The ReportLab module is missing. Download it from http://www.reportlab.org"
183        if not hasPIL :
184            raise PyKotaToolError, "The Python Imaging Library is missing. Download it from http://www.pythonware.com/downloads"
185
186        self.adminOnly()
187
188        self.pagesize = getPageSize(options.pagesize)
189
190        if (not options.reason) or (not options.reason.strip()) :
191            raise PyKotaCommandLineError, _("Refunding for no reason is forbidden. Please use the --reason command line option.")
192
193        extractonly = {}
194        for filterexp in arguments :
195            if filterexp.strip() :
196                try :
197                    (filterkey, filtervalue) = [part.strip() for part in filterexp.split("=")]
198                    filterkey = filterkey.lower()
199                    if filterkey not in self.validfilterkeys :
200                        raise ValueError
201                except ValueError :
202                    raise PyKotaCommandLineError, _("Invalid filter value [%s], see help.") % filterexp
203                else :
204                    extractonly.update({ filterkey : filtervalue })
205
206        percent = Percent(self)
207        outfname = options.output.strip().encode(sys.getfilesystemencoding())
208        if outfname != "-" :
209            percent.display("%s..." % _("Extracting datas"))
210        else :
211            options.force = True
212            self.printInfo(_("The PDF file containing the receipts will be sent to stdout. --force is assumed."), "warn")
213
214        username = extractonly.get("username")
215        if username :
216            user = self.storage.getUser(username)
217        else :
218            user = None
219
220        printername = extractonly.get("printername")
221        if printername :
222            printer = self.storage.getPrinter(printername)
223        else :
224            printer = None
225
226        start = extractonly.get("start")
227        end = extractonly.get("end")
228        (start, end) = self.storage.cleanDates(start, end)
229
230        jobs = self.storage.retrieveHistory(user=user,
231                                            printer=printer,
232                                            hostname=extractonly.get("hostname"),
233                                            billingcode=extractonly.get("billingcode"),
234                                            jobid=extractonly.get("jobid"),
235                                            start=start,
236                                            end=end,
237                                            limit=0)
238
239        peruser = {}
240        nbjobs = 0
241        nbpages = 0
242        nbcredits = 0.0
243        percent.setSize(len(jobs))
244        if outfname != "-" :
245            percent.display("\n")
246        for job in jobs :
247            if job.JobSize and (job.JobAction not in ("DENY", "CANCEL", "REFUND")) :
248                if options.force :
249                    nbpages += job.JobSize
250                    nbcredits += job.JobPrice
251                    counters = peruser.setdefault(job.UserName, { "nbjobs" : 0, "nbpages" : 0, "nbcredits" : 0.0 })
252                    counters["nbpages"] += job.JobSize
253                    counters["nbcredits"] += job.JobPrice
254                    job.refund(options.reason)
255                    counters["nbjobs"] += 1
256                    nbjobs += 1
257                    if outfname != "-" :
258                        percent.oneMore()
259                else :
260                    self.display("%s\n" % (_("Date : %s") % str(job.JobDate)[:19]))
261                    self.display("%s\n" % (_("Printer : %s") % job.PrinterName))
262                    self.display("%s\n" % (_("User : %s") % job.UserName))
263                    self.display("%s\n" % (_("JobId : %s") % job.JobId))
264                    self.display("%s\n" % (_("Title : %s") % job.JobTitle))
265                    if job.JobBillingCode :
266                        self.display("%s\n" % (_("Billing code : %s") % job.JobBillingCode))
267                    self.display("%s\n" % (_("Pages : %i") % job.JobSize))
268                    self.display("%s\n" % (_("Credits : %.3f") % job.JobPrice))
269
270                    while True :
271                        answer = raw_input("\t%s ? " % _("Refund (Y/N)")).strip().upper()
272                        if answer == _("Y") :
273                            nbpages += job.JobSize
274                            nbcredits += job.JobPrice
275                            counters = peruser.setdefault(job.UserName, { "nbjobs" : 0, "nbpages" : 0, "nbcredits" : 0.0 })
276                            counters["nbpages"] += job.JobSize
277                            counters["nbcredits"] += job.JobPrice
278                            job.refund(options.reason)
279                            counters["nbjobs"] += 1
280                            nbjobs += 1
281                            break
282                        elif answer == _("N") :
283                            break
284                    print
285        if outfname != "-" :
286            percent.done()
287        self.genReceipts(peruser,
288                         options.logo.strip().encode(sys.getfilesystemencoding()),
289                         outfname,
290                         options.number,
291                         options.reason,
292                         options.unit or _("Credits"))
293        if outfname != "-" :
294            nbusers = len(peruser)
295            self.display("%s\n" % (_("Refunded %(nbusers)i users for %(nbjobs)i jobs, %(nbpages)i pages and %(nbcredits).3f credits") \
296                     % locals()))
297
298if __name__ == "__main__" :
299    parser = PyKotaOptionParser(description=_("Refunding tool for PyKota."),
300                                usage="pkrefund [options] [filterexpr]")
301    parser.add_option("-f", "--force",
302                            dest="force",
303                            action="store_true",
304                            help=_("Doesn't ask for confirmation before refunding."))
305    parser.add_option("-l", "--logo",
306                            dest="logo",
307                            default=u"/usr/share/pykota/logos/pykota.jpeg",
308                            help=_("The image to use as a logo. The logo will be drawn at the center top of the page. The default logo is %default."))
309    parser.add_option("-p", "--pagesize",
310                            type="string",
311                            action="callback",
312                            callback=checkandset_pagesize,
313                            dest="pagesize",
314                            default=u"A4",
315                            help=_("Set the size of the page. Most well known page sizes are recognized, like 'A4' or 'Letter' to name a few. The default page size is %default."))
316    parser.add_option("-n", "--number",
317                            dest="number",
318                            type="int",
319                            action="callback",
320                            callback=checkandset_positiveint,
321                            default=1,
322                            help=_("Sets the number of the first receipt. This number will automatically be incremented for each receipt. The default value is %default."))
323    parser.add_option("-o", "--output",
324                            dest="output",
325                            type="string",
326                            default=u"-",
327                            help=_("The name of the file to which the PDF receipts will be written. If not set or set to '%default', the PDF document will be sent to the standard output."))
328    parser.add_option("-r", "--reason",
329                            dest="reason",
330                            type="string",
331                            help=_("The reason why there was a refund."))
332
333    # TODO : due to Python's optparse.py bug #1498146 fixed in rev 46861
334    # TODO : we can't use 'default=_("Credits")' for this option
335    parser.add_option("-u", "--unit",
336                            dest="unit",
337                            type="string",
338                            help=_("The name of the unit to use on the receipts. The default value is 'Credits' or its locale translation."))
339
340    parser.add_filterexpression("username", _("User's name"))
341    parser.add_filterexpression("printername", _("Printer's name"))
342    parser.add_filterexpression("hostname", _("Host's name"))
343    parser.add_filterexpression("jobid", _("Job's id"))
344    parser.add_filterexpression("billingcode", _("Job's billing code"))
345    parser.add_filterexpression("start", _("Job's date of printing"))
346    parser.add_filterexpression("end", _("Job's date of printing"))
347
348    parser.add_example('--output /tmp/receipts.pdf jobid=503',
349                       _("This would refund all jobs which Id is 503. A confirmation would be asked for each job to refund, and a PDF file named /tmp/receipts.pdf would be created containing printable receipts. BEWARE of job ids rolling over if you reset CUPS' history."))
350
351    parser.add_example('--reason "Hardware problem" jobid=503 start=today-7',
352                       _("This would refund all jobs which id is 503 but which would have been printed during the  past week. The reason would be marked as being an hardware problem."))
353
354    parser.add_example('--force username=jerome printername=HP2100',
355                       _("This would refund all jobs printed by user jerome on printer HP2100. No confirmation would be asked."))
356
357    parser.add_example('--force printername=HP2100 start=200602 end=yesterday',
358                       _("This would refund all jobs printed on printer HP2100 between February 1st 2006 and yesterday. No confirmation would be asked."))
359
360    (options, arguments) = parser.parse_args()
361    run(parser, PKRefund)
Note: See TracBrowser for help on using the browser.