root / pykota / trunk / bin / pkinvoice @ 3288

Revision 3288, 16.7 kB (checked in by jerome, 16 years ago)

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