root / pykota / trunk / bin / pkinvoice @ 3118

Revision 3090, 16.8 kB (checked in by jerome, 18 years ago)

Now pkinvoice works internally like pkrefund. Huge code
factorization is now possible (but will wait next
development cycle).

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