root / pykota / trunk / bin / pkinvoice @ 3294

Revision 3294, 16.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_("""pkinvoice v%(__version__)s (c) %(__years__)s %(__author__)s
53
54An invoice generator for PyKota.
55
56command line usage :
57
58  pkinvoice [options] [filterexpr]
59
60options :
61
62  -v | --version       Prints pkinvoice's version number then exits.
63  -h | --help          Prints this message then exits.
64 
65  -l | --logo img      Use the image as the invoice's logo. The logo will
66                       be drawn at the center top of the page. The default
67                       logo is /usr/share/pykota/logos/pykota.jpeg
68                       
69  -p | --pagesize sz   Sets sz as the page size. Most well known
70                       page sizes are recognized, like 'A4' or 'Letter'
71                       to name a few. The default size is A4.
72                       
73  -n | --number N      Sets the number of the first invoice. This number
74                       will automatically be incremented for each invoice.
75                       
76  -o | --output f.pdf  Defines the name of the invoice file which will
77                       be generated as a PDF document. If not set or
78                       set to '-', the PDF document is sent to standard
79                       output.
80                       
81  -u | --unit u        Defines the name of the unit to use on the invoice.                       
82                       The default unit is 'Credits', optionally translated
83                       to your native language if it is supported by PyKota.
84 
85  -V | --vat p         Sets the percent value of the applicable VAT to be
86                       exposed. The default is 0.0, meaning no VAT
87                       information will be included.
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  $ pkinvoice --unit EURO --output /tmp/invoices.pdf start=now-30
129 
130  Will generate a PDF document containing invoices for all users
131  who have spent some credits last month. Invoices will be done in
132  EURO.  No VAT information will be included.
133""") 
134       
135class PKInvoice(PyKotaTool) :       
136    """A class for invoice generator."""
137    validfilterkeys = [ "username",
138                        "printername",
139                        "hostname",
140                        "jobid",
141                        "billingcode",
142                        "start",
143                        "end",
144                      ]
145                     
146    def getPageSize(self, pgsize) :
147        """Returns the correct page size or None if not found."""
148        try :
149            return getattr(pagesizes, pgsize.upper())
150        except AttributeError :   
151            try :
152                return getattr(pagesizes, pgsize.lower())
153            except AttributeError :
154                pass
155               
156    def printVar(self, label, value, size) :
157        """Outputs a variable onto the PDF canvas.
158       
159           Returns the number of points to substract to current Y coordinate.
160        """   
161        xcenter = (self.pagesize[0] / 2.0) - 1*cm
162        self.canvas.saveState()
163        self.canvas.setFont("Helvetica-Bold", size)
164        self.canvas.setFillColorRGB(0, 0, 0)
165        self.canvas.drawRightString(xcenter, self.ypos, "%s :" % self.userCharsetToUTF8(label))
166        self.canvas.setFont("Courier-Bold", size)
167        self.canvas.setFillColorRGB(0, 0, 1)
168        self.canvas.drawString(xcenter + 0.5*cm, self.ypos, self.userCharsetToUTF8(value))
169        self.canvas.restoreState()
170        self.ypos -= (size + 4)
171       
172    def pagePDF(self, invoicenumber, name, values, unit, vat) :
173        """Generates a new page in the PDF document."""
174        amount = values["nbcredits"]
175        if amount : # is there's something due ?
176            ht = ((amount * 10000.0) / (100.0 + vat)) / 100.0
177            vatamount = amount - ht
178            self.canvas.doForm("background")
179            self.ypos = self.yorigine - (cm + 20)
180            self.printVar(_("Invoice"), "#%s" % invoicenumber, 22)
181            self.printVar(_("Username"), name, 22)
182            self.ypos -= 20
183            self.printVar(_("Edited on"), time.strftime("%c", time.localtime()), 14)
184               
185            self.ypos -= 20
186            self.printVar(_("Number of jobs printed"), str(values["nbjobs"]), 18)
187            self.printVar(_("Number of pages printed"), str(values["nbpages"]), 18)
188            self.ypos -= 20
189            self.printVar(_("Amount due"), "%.3f %s" % (amount, unit), 18)
190            if vat :
191                self.ypos += 8
192                self.printVar("%s (%.2f%%)" % (_("Included VAT"), vat), "%.3f %s" % (vatamount, unit), 14)
193            self.canvas.showPage()
194            return 1
195        return 0   
196       
197    def initPDF(self, logo) :
198        """Initializes the PDF document."""
199        self.pdfDocument = cStringIO.StringIO()       
200        self.canvas = c = canvas.Canvas(self.pdfDocument, \
201                                        pagesize=self.pagesize, \
202                                        pageCompression=1)
203       
204        c.setAuthor(self.effectiveUserName)
205        c.setTitle("PyKota invoices")
206        c.setSubject("Invoices generated with PyKota")
207       
208        self.canvas.beginForm("background")
209        self.canvas.saveState()
210       
211        self.ypos = self.pagesize[1] - (2 * cm)           
212       
213        xcenter = self.pagesize[0] / 2.0
214        if logo :
215            try :   
216                imglogo = PIL.Image.open(logo)
217            except IOError :   
218                self.printInfo("Unable to open image %s" % logo, "warn")
219            else :
220                (width, height) = imglogo.size
221                multi = float(width) / (8 * cm) 
222                width = float(width) / multi
223                height = float(height) / multi
224                self.ypos -= height
225                c.drawImage(logo, xcenter - (width / 2.0), \
226                                  self.ypos, \
227                                  width, height)
228       
229        self.ypos -= (cm + 20)
230        self.canvas.setFont("Helvetica-Bold", 14)
231        self.canvas.setFillColorRGB(0, 0, 0)
232        msg = _("Here's the invoice for your printouts")
233        self.canvas.drawCentredString(xcenter, self.ypos, "%s :" % self.userCharsetToUTF8(msg))
234       
235        self.yorigine = self.ypos
236        self.canvas.restoreState()
237        self.canvas.endForm()
238       
239    def endPDF(self, fname) :   
240        """Flushes the PDF generator."""
241        self.canvas.save()
242        if fname != "-" :       
243            outfile = open(fname, "w")
244            outfile.write(self.pdfDocument.getvalue())
245            outfile.close()
246        else :   
247            sys.stdout.write(self.pdfDocument.getvalue())
248            sys.stdout.flush()
249       
250    def genInvoices(self, peruser, logo, outfname, firstnumber, unit, vat) :
251        """Generates the invoices file."""
252        if len(peruser) :
253            percent = Percent(self)
254            percent.setSize(len(peruser))
255            if outfname != "-" :
256                percent.display("%s...\n" % _("Generating invoices"))
257               
258            self.initPDF(logo)
259            number = firstnumber
260            for (name, values) in peruser.items() :
261                number += self.pagePDF(number, name, values, unit, vat)
262                if outfname != "-" :
263                    percent.oneMore()
264                   
265            if number > firstnumber :
266                self.endPDF(outfname)
267               
268            if outfname != "-" :
269                percent.done()
270       
271    def main(self, arguments, options) :
272        """Generate invoices."""
273        if not hasRL :
274            raise PyKotaToolError, "The ReportLab module is missing. Download it from http://www.reportlab.org"
275        if not hasPIL :
276            raise PyKotaToolError, "The Python Imaging Library is missing. Download it from http://www.pythonware.com/downloads"
277           
278        if not self.config.isAdmin :
279            raise PyKotaCommandLineError, "%s : %s" % (pwd.getpwuid(os.geteuid())[0], _("You're not allowed to use this command."))
280       
281        try :   
282            vat = float(options["vat"])
283            if not (0.0 <= vat < 100.0) :
284                raise ValueError
285        except :   
286            raise PyKotaCommandLineError, _("Incorrect value '%s' for the --vat command line option") % options["vat"]
287           
288        try :   
289            firstnumber = number = int(options["number"])
290            if number <= 0 :
291                raise ValueError
292        except :   
293            raise PyKotaCommandLineError, _("Incorrect value '%s' for the --number command line option") % options["number"]
294           
295        self.pagesize = self.getPageSize(options["pagesize"])
296        if self.pagesize is None :
297            self.pagesize = self.getPageSize("a4")
298            self.printInfo(_("Invalid 'pagesize' option %s, defaulting to A4.") % options["pagesize"], "warn")
299           
300        extractonly = {}
301        for filterexp in arguments :
302            if filterexp.strip() :
303                try :
304                    (filterkey, filtervalue) = [part.strip() for part in filterexp.split("=")]
305                    filterkey = filterkey.lower()
306                    if filterkey not in self.validfilterkeys :
307                        raise ValueError               
308                except ValueError :   
309                    raise PyKotaCommandLineError, _("Invalid filter value [%s], see help.") % filterexp
310                else :   
311                    extractonly.update({ filterkey : filtervalue })
312           
313        percent = Percent(self)
314        outfname = options["output"].strip()
315        if outfname != "-" :
316            percent.display("%s..." % _("Extracting datas"))
317           
318        username = extractonly.get("username")   
319        if username :
320            user = self.storage.getUser(username)
321        else :
322            user = None
323           
324        printername = extractonly.get("printername")   
325        if printername :
326            printer = self.storage.getPrinter(printername)
327        else :   
328            printer = None
329           
330        start = extractonly.get("start")
331        end = extractonly.get("end")
332        (start, end) = self.storage.cleanDates(start, end)
333       
334        jobs = self.storage.retrieveHistory(user=user,   
335                                            printer=printer, 
336                                            hostname=extractonly.get("hostname"),
337                                            billingcode=extractonly.get("billingcode"),
338                                            jobid=extractonly.get("jobid"),
339                                            start=start,
340                                            end=end,
341                                            limit=0)
342           
343        peruser = {}                                   
344        nbjobs = 0                                   
345        nbpages = 0                                           
346        nbcredits = 0.0
347        percent.setSize(len(jobs))
348        if outfname != "-" :
349            percent.display("\n")
350        for job in jobs :                                   
351            if job.JobSize and (job.JobAction not in ("DENY", "CANCEL", "REFUND")) :
352                nbpages += job.JobSize
353                nbcredits += job.JobPrice
354                counters = peruser.setdefault(job.UserName, { "nbjobs" : 0, "nbpages" : 0, "nbcredits" : 0.0 })
355                counters["nbpages"] += job.JobSize
356                counters["nbcredits"] += job.JobPrice
357                counters["nbjobs"] += 1
358                nbjobs += 1
359                if outfname != "-" :
360                    percent.oneMore()
361        if outfname != "-" :
362            percent.done()
363        self.genInvoices(peruser, options["logo"].strip(), outfname, firstnumber, options["unit"], vat)
364        if outfname != "-" :   
365            print _("Invoiced %i users for %i jobs, %i pages and %.3f credits") % (len(peruser), nbjobs, nbpages, nbcredits)
366                     
367if __name__ == "__main__" : 
368    retcode = 0
369    try :
370        defaults = { "vat" : "0.0",
371                     "unit" : N_("Credits"),
372                     "output" : "-",
373                     "pagesize" : "a4", \
374                     "logo" : "/usr/share/pykota/logos/pykota.jpeg",
375                     "number" : "1",
376                   }
377        short_options = "vho:u:V:p:l:n:"
378        long_options = ["help", "version", "unit=", "output=", \
379                        "pagesize=", "logo=", "vat=", "number="]
380       
381        # Initializes the command line tool
382        invoiceGenerator = PKInvoice(doc=__doc__)
383        invoiceGenerator.deferredInit()
384       
385        # parse and checks the command line
386        (options, args) = invoiceGenerator.parseCommandline(sys.argv[1:], short_options, long_options, allownothing=True)
387       
388        # sets long options
389        options["help"] = options["h"] or options["help"]
390        options["version"] = options["v"] or options["version"]
391       
392        options["vat"] = options["V"] or options["vat"] or defaults["vat"]
393        options["unit"] = options["u"] or options["unit"] or defaults["unit"]
394        options["output"] = options["o"] or options["output"] or defaults["output"]
395        options["pagesize"] = options["p"] or options["pagesize"] or defaults["pagesize"]
396        options["number"] = options["n"] or options["number"] or defaults["number"]
397        options["logo"] = options["l"] or options["logo"]
398        if options["logo"] is None : # Allows --logo="" to disable the logo entirely
399            options["logo"] = defaults["logo"] 
400       
401        if options["help"] :
402            invoiceGenerator.display_usage_and_quit()
403        elif options["version"] :
404            invoiceGenerator.display_version_and_quit()
405        else :
406            retcode = invoiceGenerator.main(args, options)
407    except KeyboardInterrupt :       
408        logerr("\nInterrupted with Ctrl+C !\n")
409        retcode = -3
410    except PyKotaCommandLineError, msg :     
411        logerr("%s : %s\n" % (sys.argv[0], msg))
412        retcode = -2
413    except SystemExit :       
414        pass
415    except :
416        try :
417            invoiceGenerator.crashed("pkinvoice failed")
418        except :   
419            crashed("pkinvoice failed")
420        retcode = -1
421
422    try :
423        invoiceGenerator.storage.close()
424    except (TypeError, NameError, AttributeError) :   
425        pass
426       
427    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.