root / pykota / trunk / bin / pkrefund

Revision 3561, 17.5 kB (checked in by jerome, 12 years ago)

Changed copyright years.

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