root / pykota / trunk / bin / pkinvoice @ 3260

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

Changed license to GNU GPL v3 or later.
Changed Python source encoding from ISO-8859-15 to UTF-8 (only ASCII
was used anyway).

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