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

"""pkrefund is a tool to refund print jobs and generate PDF receipts."""

# PyKota Print Job Refund Manager
#
# 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: pkrefund 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_("""pkrefund v%(__version__)s (c) %(__years__)s %(__author__)s

Refunds jobs.

command line usage :

  pkrefund [options] [filterexpr]

options :

  -v | --version       Prints pkrefund's version number then exits.
  -h | --help          Prints this message then exits.
  
  -f | --force         Doesn't ask for confirmation before refunding jobs.
  -r | --reason txt    Sets textual information to explain the refunding.

  -l | --logo img      Use the image as the receipt'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 receipt. This number
                       will automatically be incremented for each receipt.

  -o | --output f.pdf  Defines the name of the PDF file which will contain
                       the receipts. If not set, then no PDF file will
                       be created. If set to '-', then --force is assumed,
                       and the PDF document is sent to standard output.

  -u | --unit u        Defines the name of the unit to use on the receipts.
                       The default unit is 'Credits', optionally translated
                       to your native language if it is supported by PyKota.
  

  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 :

  $ pkrefund --output /tmp/receipts.pdf jobid=503
  
  This will refund all jobs which Id is 503. BEWARE : installing CUPS
  afresh will reset the first job id at 1, so you probably want to use
  a more precise filter as explained below. A confirmation will
  be asked for each job to refund, and a PDF file named /tmp/receipts.pdf
  will be created which will contain printable receipts.
  
  $ pkrefund --reason "Hardware problem" jobid=503 start=today-7
  
  Refunds all jobs which id is 503 but which were printed during the
  past week. The reason will be marked as being an hardware problem.
  
  $ pkrefund --force username=jerome printername=HP2100
  
  Refunds all jobs printed by user jerome on printer HP2100. No
  confirmation will be asked.
  
  $ pkrefund --force printername=HP2100 start=200602 end=yesterday
  
  Refunds all jobs printed on printer HP2100 between February 1st 2006
  and yesterday. No confirmation will be asked.
""")
        
class PkRefund(PyKotaTool) :        
    """A class for refund manager."""
    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, receiptnumber, name, values, unit, reason) :
        """Generates a new page in the PDF document."""
        if values["nbpages"] :
            self.canvas.doForm("background")
            self.ypos = self.yorigine - (cm + 20)
            self.printVar(_("Refunding receipt"), "#%s" % receiptnumber, 22)
            self.printVar(_("Username"), name, 22)
            self.ypos -= 20
            self.printVar(_("Edited on"), time.strftime("%c", time.localtime()), 14)
                
            self.ypos -= 20
            self.printVar(_("Jobs refunded"), str(values["nbjobs"]), 18)
            self.printVar(_("Pages refunded"), str(values["nbpages"]), 18)
            self.printVar(_("Amount refunded"), "%.3f %s" % (values["nbcredits"], unit), 18)
            self.ypos -= 20
            self.printVar(_("Reason"), reason, 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 print job refunding receipts")
        c.setSubject("Print job refunding receipts 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 receipt for the refunding of your print jobs")
        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 genReceipts(self, peruser, logo, outfname, firstnumber, reason, unit) :
        """Generates the receipts file."""
        if outfname and len(peruser) :
            percent = Percent(self, size=len(peruser))
            if outfname != "-" :
                percent.display("%s...\n" % _("Generating receipts"))
                
            self.initPDF(logo)
            number = firstnumber
            for (name, values) in peruser.items() :
                number += self.pagePDF(number, name, values, unit, reason)
                if outfname != "-" :
                    percent.oneMore()
                    
            if number > firstnumber :
                self.endPDF(outfname)
                
            if outfname != "-" :
                percent.done()
        
    def main(self, arguments, options, restricted=1) :
        """Print Quota Data Dumper."""
        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 restricted and not self.config.isAdmin :
            raise PyKotaCommandLineError, "%s : %s" % (pwd.getpwuid(os.geteuid())[0], _("You're not allowed to use this command."))
            
        if (not options["reason"]) or not options["reason"].strip() :
            raise PyKotaCommandLineError, _("Refunding for no reason is forbidden. Please use the --reason command line option.")
            
        outfname = options["output"]
        if outfname :
            outfname = outfname.strip()
            if outfname == "-" :
                options["force"] = True
                self.printInfo(_("The PDF file containing the receipts will be sent to stdout. --force is assumed."), "warn")
            
        try :    
            firstnumber = int(options["number"])
            if firstnumber <= 0 :
                raise ValueError
        except (ValueError, TypeError) :    
            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)
        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
        reason = (options.get("reason") or "").strip()
        percent.setSize(len(jobs))
        if outfname != "-" :
            percent.display("\n")
        for job in jobs :                                    
            if job.JobSize and (job.JobAction not in ("DENY", "CANCEL", "REFUND")) :
                if options["force"] :
                    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
                    job.refund(reason)
                    counters["nbjobs"] += 1
                    nbjobs += 1
                    if outfname != "-" :
                        percent.oneMore()
                else :    
                    print _("Date : %s") % str(job.JobDate)[:19]
                    print _("Printer : %s") % job.PrinterName
                    print _("User : %s") % job.UserName
                    print _("JobId : %s") % job.JobId
                    print _("Title : %s") % job.JobTitle
                    if job.JobBillingCode :
                        print _("Billing code : %s") % job.JobBillingCode
                    print _("Pages : %i") % job.JobSize
                    print _("Credits : %.3f") % job.JobPrice
                    
                    while True :                             
                        answer = raw_input("\t%s ? " % _("Refund (Y/N)")).strip().upper()
                        if answer == _("Y") :
                            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
                            job.refund(reason)
                            counters["nbjobs"] += 1
                            nbjobs += 1
                            break    
                        elif answer == _("N") :    
                            break
                    print        
        if outfname != "-" :
            percent.done()
        self.genReceipts(peruser, options["logo"].strip(), outfname, firstnumber, reason, options["unit"])
        if outfname != "-" :    
            print _("Refunded %i users for %i jobs, %i pages and %.3f credits") % (len(peruser), nbjobs, nbpages, nbcredits)
            
if __name__ == "__main__" : 
    retcode = 0
    try :
        defaults = { "unit" : N_("Credits"),
                     "pagesize" : "a4", \
                     "logo" : "/usr/share/pykota/logos/pykota.jpeg",
                     "number" : "1",
                   }
        short_options = "vhfru:o:p:l:n:"
        long_options = ["help", "version", "force", "reason=", "unit=", "output=", "pagesize=", "logo=", "number="]
        
        # Initializes the command line tool
        refundmanager = PkRefund(doc=__doc__)
        refundmanager.deferredInit()
        
        # parse and checks the command line
        (options, args) = refundmanager.parseCommandline(sys.argv[1:], short_options, long_options, allownothing=1)
        
        # sets long options
        options["help"] = options["h"] or options["help"]
        options["version"] = options["v"] or options["version"]
        options["force"] = options["f"] or options["force"]
        options["reason"] = options["r"] or options["reason"]
        options["unit"] = options["u"] or options["unit"] or defaults["unit"]
        options["output"] = options["o"] or options["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"] :
            refundmanager.display_usage_and_quit()
        elif options["version"] :
            refundmanager.display_version_and_quit()
        else :
            retcode = refundmanager.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 :
            refundmanager.crashed("pkrefund failed")
        except :    
            crashed("pkrefund failed")
        retcode = -1

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