#! /usr/bin/env python
# -*- coding: ISO-8859-15 -*-

# PyKota Invoice generator
#
# PyKota - Print Quotas for CUPS and LPRng
#
# (c) 2003, 2004, 2005, 2006, 2007 Jerome Alet <alet@librelogiciel.com>
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# $Id: pkinvoice 3133 2007-01-17 22:19:42Z jerome $
#
#

import sys
import os
import pwd
import time
import cStringIO

try :
    from reportlab.pdfgen import canvas
    from reportlab.lib import pagesizes
    from reportlab.lib.units import cm
except ImportError :    
    hasRL = 0
else :    
    hasRL = 1
    
try :
    import PIL.Image 
except ImportError :    
    hasPIL = 0
else :    
    hasPIL = 1

from pykota.tool import Percent, PyKotaTool, PyKotaToolError, PyKotaCommandLineError, crashed, N_

__doc__ = N_("""pkinvoice v%(__version__)s (c) %(__years__)s %(__author__)s

An invoice generator for PyKota.

command line usage :

  pkinvoice [options] [filterexpr]

options :

  -v | --version       Prints pkinvoice's version number then exits.
  -h | --help          Prints this message then exits.
  
  -l | --logo img      Use the image as the invoice's logo. The logo will
                       be drawn at the center top of the page. The default
                       logo is /usr/share/pykota/logos/pykota.jpeg
                       
  -p | --pagesize sz   Sets sz as the page size. Most well known
                       page sizes are recognized, like 'A4' or 'Letter'
                       to name a few. The default size is A4.
                       
  -n | --number N      Sets the number of the first invoice. This number
                       will automatically be incremented for each invoice.
                       
  -o | --output f.pdf  Defines the name of the invoice file which will
                       be generated as a PDF document. If not set or
                       set to '-', the PDF document is sent to standard
                       output. 
                       
  -u | --unit u        Defines the name of the unit to use on the invoice.                       
                       The default unit is 'Credits', optionally translated
                       to your native language if it is supported by PyKota.
  
  -V | --vat p         Sets the percent value of the applicable VAT to be
                       exposed. The default is 0.0, meaning no VAT
                       information will be included.
  

  Use the filter expressions to extract only parts of the 
  datas. Allowed filters are of the form :
                
         key=value
                         
  Allowed keys for now are :  
                       
         username       User's name
         printername    Printer's name
         hostname       Client's hostname
         jobid          Job's Id
         billingcode    Job's billing code
         start          Job's date of printing
         end            Job's date of printing
         
  Dates formatting with 'start' and 'end' filter keys :
  
    YYYY : year boundaries
    YYYYMM : month boundaries
    YYYYMMDD : day boundaries
    YYYYMMDDhh : hour boundaries
    YYYYMMDDhhmm : minute boundaries
    YYYYMMDDhhmmss : second boundaries
    yesterday[+-NbDays] : yesterday more or less N days (e.g. : yesterday-15)
    today[+-NbDays] : today more or less N days (e.g. : today-15)
    tomorrow[+-NbDays] : tomorrow more or less N days (e.g. : tomorrow-15)
    now[+-NbDays] : now more or less N days (e.g. now-15)

  'now' and 'today' are not exactly the same since today represents the first
  or last second of the day depending on if it's used in a start= or end=
  date expression. The utility to be able to specify dates in the future is
  a question which remains to be answered :-)
  
  Contrary to other PyKota management tools, wildcard characters are not 
  expanded, so you can't use them.
  
examples :

  $ pkinvoice --unit EURO --output /tmp/invoices.pdf start=now-30
  
  Will generate a PDF document containing invoices for all users
  who have spent some credits last month. Invoices will be done in
  EURO.  No VAT information will be included.
""") 
        
class PKInvoice(PyKotaTool) :        
    """A class for invoice generator."""
    validfilterkeys = [ "username",
                        "printername",
                        "hostname",
                        "jobid",
                        "billingcode",
                        "start",
                        "end",
                      ]
                      
    def getPageSize(self, pgsize) :
        """Returns the correct page size or None if not found."""
        try :
            return getattr(pagesizes, pgsize.upper())
        except AttributeError :    
            try :
                return getattr(pagesizes, pgsize.lower())
            except AttributeError :
                pass
                
    def printVar(self, label, value, size) :
        """Outputs a variable onto the PDF canvas.
        
           Returns the number of points to substract to current Y coordinate.
        """   
        xcenter = (self.pagesize[0] / 2.0) - 1*cm
        self.canvas.saveState()
        self.canvas.setFont("Helvetica-Bold", size)
        self.canvas.setFillColorRGB(0, 0, 0)
        self.canvas.drawRightString(xcenter, self.ypos, "%s :" % self.userCharsetToUTF8(label))
        self.canvas.setFont("Courier-Bold", size)
        self.canvas.setFillColorRGB(0, 0, 1)
        self.canvas.drawString(xcenter + 0.5*cm, self.ypos, self.userCharsetToUTF8(value))
        self.canvas.restoreState()
        self.ypos -= (size + 4)
        
    def pagePDF(self, invoicenumber, name, values, unit, vat) :
        """Generates a new page in the PDF document."""
        amount = values["nbcredits"]
        if amount : # is there's something due ?
            ht = ((amount * 10000.0) / (100.0 + vat)) / 100.0
            vatamount = amount - ht
            self.canvas.doForm("background")
            self.ypos = self.yorigine - (cm + 20)
            self.printVar(_("Invoice"), "#%s" % invoicenumber, 22)
            self.printVar(_("Username"), name, 22)
            self.ypos -= 20
            self.printVar(_("Edited on"), time.strftime("%c", time.localtime()), 14)
                
            self.ypos -= 20
            self.printVar(_("Number of jobs printed"), str(values["nbjobs"]), 18)
            self.printVar(_("Number of pages printed"), str(values["nbpages"]), 18)
            self.ypos -= 20
            self.printVar(_("Amount due"), "%.3f %s" % (amount, unit), 18)
            if vat :
                self.ypos += 8
                self.printVar("%s (%.2f%%)" % (_("Included VAT"), vat), "%.3f %s" % (vatamount, unit), 14)
            self.canvas.showPage()
            return 1
        return 0    
        
    def initPDF(self, logo) :
        """Initializes the PDF document."""
        self.pdfDocument = cStringIO.StringIO()        
        self.canvas = c = canvas.Canvas(self.pdfDocument, \
                                        pagesize=self.pagesize, \
                                        pageCompression=1)
        
        c.setAuthor(self.originalUserName)
        c.setTitle("PyKota invoices")
        c.setSubject("Invoices generated with PyKota")
        
        self.canvas.beginForm("background")
        self.canvas.saveState()
        
        self.ypos = self.pagesize[1] - (2 * cm)            
        
        xcenter = self.pagesize[0] / 2.0
        if logo :
            try :    
                imglogo = PIL.Image.open(logo)
            except IOError :    
                self.printInfo("Unable to open image %s" % logo, "warn")
            else :
                (width, height) = imglogo.size
                multi = float(width) / (8 * cm) 
                width = float(width) / multi
                height = float(height) / multi
                self.ypos -= height
                c.drawImage(logo, xcenter - (width / 2.0), \
                                  self.ypos, \
                                  width, height)
        
        self.ypos -= (cm + 20)
        self.canvas.setFont("Helvetica-Bold", 14)
        self.canvas.setFillColorRGB(0, 0, 0)
        msg = _("Here's the invoice for your printouts")
        self.canvas.drawCentredString(xcenter, self.ypos, "%s :" % self.userCharsetToUTF8(msg))
        
        self.yorigine = self.ypos
        self.canvas.restoreState()
        self.canvas.endForm()
        
    def endPDF(self, fname) :    
        """Flushes the PDF generator."""
        self.canvas.save()
        if fname != "-" :        
            outfile = open(fname, "w")
            outfile.write(self.pdfDocument.getvalue())
            outfile.close()
        else :    
            sys.stdout.write(self.pdfDocument.getvalue())
            sys.stdout.flush()
        
    def genInvoices(self, peruser, logo, outfname, firstnumber, unit, vat) :
        """Generates the invoices file."""
        if len(peruser) :
            percent = Percent(self)
            percent.setSize(len(peruser))
            if outfname != "-" :
                percent.display("%s...\n" % _("Generating invoices"))
                
            self.initPDF(logo)
            number = firstnumber
            for (name, values) in peruser.items() :
                number += self.pagePDF(number, name, values, unit, vat)
                if outfname != "-" :
                    percent.oneMore()
                    
            if number > firstnumber :
                self.endPDF(outfname)
                
            if outfname != "-" :
                percent.done()
        
    def main(self, arguments, options) :
        """Generate invoices."""
        if not hasRL :
            raise PyKotaToolError, "The ReportLab module is missing. Download it from http://www.reportlab.org"
        if not hasPIL :
            raise PyKotaToolError, "The Python Imaging Library is missing. Download it from http://www.pythonware.com/downloads"
            
        if not self.config.isAdmin :
            raise PyKotaCommandLineError, "%s : %s" % (pwd.getpwuid(os.geteuid())[0], _("You're not allowed to use this command."))
        
        try :    
            vat = float(options["vat"])
            if not (0.0 <= vat < 100.0) :
                raise ValueError
        except :    
            raise PyKotaCommandLineError, _("Incorrect value '%s' for the --vat command line option") % options["vat"]
            
        try :    
            firstnumber = number = int(options["number"])
            if number <= 0 :
                raise ValueError
        except :    
            raise PyKotaCommandLineError, _("Incorrect value '%s' for the --number command line option") % options["number"]
            
        self.pagesize = self.getPageSize(options["pagesize"])
        if self.pagesize is None :
            self.pagesize = self.getPageSize("a4")
            self.printInfo(_("Invalid 'pagesize' option %s, defaulting to A4.") % options["pagesize"], "warn")
            
        extractonly = {}
        for filterexp in arguments :
            if filterexp.strip() :
                try :
                    (filterkey, filtervalue) = [part.strip() for part in filterexp.split("=")]
                    filterkey = filterkey.lower()
                    if filterkey not in self.validfilterkeys :
                        raise ValueError                
                except ValueError :    
                    raise PyKotaCommandLineError, _("Invalid filter value [%s], see help.") % filterexp
                else :    
                    extractonly.update({ filterkey : filtervalue })
            
        percent = Percent(self)
        outfname = options["output"].strip()
        if outfname != "-" :
            percent.display("%s..." % _("Extracting datas"))
            
        username = extractonly.get("username")    
        if username :
            user = self.storage.getUser(username)
        else :
            user = None
            
        printername = extractonly.get("printername")    
        if printername :
            printer = self.storage.getPrinter(printername)
        else :    
            printer = None
            
        start = extractonly.get("start")
        end = extractonly.get("end")
        (start, end) = self.storage.cleanDates(start, end)
        
        jobs = self.storage.retrieveHistory(user=user,    
                                            printer=printer, 
                                            hostname=extractonly.get("hostname"),
                                            billingcode=extractonly.get("billingcode"),
                                            jobid=extractonly.get("jobid"),
                                            start=start,
                                            end=end,
                                            limit=0)
            
        peruser = {}                                    
        nbjobs = 0                                    
        nbpages = 0                                            
        nbcredits = 0.0
        percent.setSize(len(jobs))
        if outfname != "-" :
            percent.display("\n")
        for job in jobs :                                    
            if job.JobSize and (job.JobAction not in ("DENY", "CANCEL", "REFUND")) :
                nbpages += job.JobSize
                nbcredits += job.JobPrice
                counters = peruser.setdefault(job.UserName, { "nbjobs" : 0, "nbpages" : 0, "nbcredits" : 0.0 })
                counters["nbpages"] += job.JobSize
                counters["nbcredits"] += job.JobPrice
                counters["nbjobs"] += 1
                nbjobs += 1
                if outfname != "-" :
                    percent.oneMore()
        if outfname != "-" :
            percent.done()
        self.genInvoices(peruser, options["logo"].strip(), outfname, firstnumber, options["unit"], vat)
        if outfname != "-" :    
            print _("Invoiced %i users for %i jobs, %i pages and %.3f credits") % (len(peruser), nbjobs, nbpages, nbcredits)
                     
if __name__ == "__main__" : 
    retcode = 0
    try :
        defaults = { "vat" : "0.0",
                     "unit" : N_("Credits"),
                     "output" : "-",
                     "pagesize" : "a4", \
                     "logo" : "/usr/share/pykota/logos/pykota.jpeg",
                     "number" : "1",
                   }
        short_options = "vho:u:V:p:l:n:"
        long_options = ["help", "version", "unit=", "output=", \
                        "pagesize=", "logo=", "vat=", "number="]
        
        # Initializes the command line tool
        invoiceGenerator = PKInvoice(doc=__doc__)
        invoiceGenerator.deferredInit()
        
        # parse and checks the command line
        (options, args) = invoiceGenerator.parseCommandline(sys.argv[1:], short_options, long_options, allownothing=True)
        
        # sets long options
        options["help"] = options["h"] or options["help"]
        options["version"] = options["v"] or options["version"]
        
        options["vat"] = options["V"] or options["vat"] or defaults["vat"]
        options["unit"] = options["u"] or options["unit"] or defaults["unit"]
        options["output"] = options["o"] or options["output"] or defaults["output"]
        options["pagesize"] = options["p"] or options["pagesize"] or defaults["pagesize"]
        options["number"] = options["n"] or options["number"] or defaults["number"]
        options["logo"] = options["l"] or options["logo"]
        if options["logo"] is None : # Allows --logo="" to disable the logo entirely
            options["logo"] = defaults["logo"]  
        
        if options["help"] :
            invoiceGenerator.display_usage_and_quit()
        elif options["version"] :
            invoiceGenerator.display_version_and_quit()
        else :
            retcode = invoiceGenerator.main(args, options)
    except KeyboardInterrupt :        
        sys.stderr.write("\nInterrupted with Ctrl+C !\n")
        retcode = -3
    except PyKotaCommandLineError, msg :     
        sys.stderr.write("%s : %s\n" % (sys.argv[0], msg))
        retcode = -2
    except SystemExit :        
        pass
    except :
        try :
            invoiceGenerator.crashed("pkinvoice failed")
        except :    
            crashed("pkinvoice failed")
        retcode = -1

    try :
        invoiceGenerator.storage.close()
    except (TypeError, NameError, AttributeError) :    
        pass
        
    sys.exit(retcode)    
