Index: /pykota/trunk/pykota/storages/sql.py
===================================================================
--- /pykota/trunk/pykota/storages/sql.py (revision 2949)
+++ /pykota/trunk/pykota/storages/sql.py (revision 3056)
@@ -688,4 +688,8 @@
         self.doModify("UPDATE billingcodes SET balance=balance + %s, pagecounter=pagecounter + %s WHERE id=%s" % (self.doQuote(balance), self.doQuote(pagecounter), self.doQuote(bcode.ident)))
        
+    def refundJob(self, jobident) :   
+        """Marks a job as refunded in the history."""
+        self.doModify("UPDATE jobhistory SET action='REFUND' WHERE id=%s;" % self.doQuote(jobident))
+        
     def decreaseUserAccountBalance(self, user, amount) :    
         """Decreases user's account balance from an amount."""
@@ -763,5 +767,5 @@
         self.doModify("DELETE FROM printergroupsmembers WHERE groupid=%s AND printerid=%s" % (self.doQuote(pgroup.ident), self.doQuote(printer.ident)))
         
-    def retrieveHistory(self, user=None, printer=None, hostname=None, billingcode=None, limit=100, start=None, end=None) :
+    def retrieveHistory(self, user=None, printer=None, hostname=None, billingcode=None, jobid=None, limit=100, start=None, end=None) :
         """Retrieves all print jobs for user on printer (or all) between start and end date, limited to first 100 results."""
         query = "SELECT jobhistory.*,username,printername FROM jobhistory,users,printers WHERE users.id=userid AND printers.id=printerid"
@@ -775,4 +779,6 @@
         if billingcode is not None :    
             where.append("billingcode=%s" % self.doQuote(self.userCharsetToDatabase(billingcode)))
+        if jobid is not None :    
+            where.append("jobid=%s" % self.doQuote(jobid)) # TODO : jobid is text, so self.userCharsetToDatabase(jobid) but do all of them as well.
         if start is not None :    
             where.append("jobdate>=%s" % self.doQuote(start))
Index: /pykota/trunk/pykota/storages/ldapstorage.py
===================================================================
--- /pykota/trunk/pykota/storages/ldapstorage.py (revision 2953)
+++ /pykota/trunk/pykota/storages/ldapstorage.py (revision 3056)
@@ -1303,5 +1303,5 @@
             self.doModify(pgroup.ident, fields)         
             
-    def retrieveHistory(self, user=None, printer=None, hostname=None, billingcode=None, limit=100, start=None, end=None) :
+    def retrieveHistory(self, user=None, printer=None, hostname=None, billingcode=None, jobid=None, limit=100, start=None, end=None) :
         """Retrieves all print jobs for user on printer (or all) between start and end date, limited to first 100 results."""
         precond = "(objectClass=pykotaJob)"
@@ -1315,4 +1315,6 @@
         if billingcode is not None :
             where.append("(pykotaBillingCode=%s)" % self.userCharsetToDatabase(billingcode))
+        if jobid is not None :
+            where.append("(pykotaJobId=%s)" % jobid) # TODO : jobid is text, so self.userCharsetToDatabase(jobid) but do all of them as well.
         if where :    
             where = "(&%s)" % "".join([precond] + where)
@@ -1778,4 +1780,12 @@
         return self.doModify(bcode.ident, fields)         
 
+    def refundJob(self, jobident) :   
+        """Marks a job as refunded in the history."""
+        dn = "cn=%s,%s" % (ident, self.info["jobbase"])
+        fields = {
+                     "pykotaAction" : "REFUND",
+                 }    
+        self.doModify(dn, fields)         
+        
     def storageUserFromRecord(self, username, record) :
         """Returns a StorageUser instance from a database record."""
Index: /pykota/trunk/pykota/storage.py
===================================================================
--- /pykota/trunk/pykota/storage.py (revision 3050)
+++ /pykota/trunk/pykota/storage.py (revision 3056)
@@ -23,4 +23,6 @@
 #
 
+"""This module is the database abstraction layer for PyKota."""
+
 import os
 import imp
@@ -36,4 +38,5 @@
     __str__ = __repr__
         
+        
 class StorageObject :
     """Object present in the database."""
@@ -57,4 +60,5 @@
             getattr(self.parent, "save%s" % self.__class__.__name__[7:])(self)
             self.isDirty = False
+            
         
 class StorageUser(StorageObject) :        
@@ -124,4 +128,9 @@
         self.isDirty = False            
         
+    def refund(self, amount) :
+        """Refunds a number of credits to an user."""
+        self.consumeAccountBalance(-amount)
+        
+        
 class StorageGroup(StorageObject) :        
     """User class."""
@@ -161,4 +170,5 @@
         self.Exists = False
         self.isDirty = False            
+        
         
 class StoragePrinter(StorageObject) :
@@ -236,4 +246,5 @@
         self.Exists = False
         self.isDirty = False            
+        
         
 class StorageUserPQuota(StorageObject) :
@@ -360,4 +371,11 @@
         self.isDirty = False
         
+    def refund(self, nbpages) :    
+        """Refunds a number of pages to an user on a particular printer."""
+        self.parent.increaseUserPQuotaPagesCounters(self, -nbpages)
+        self.PageCounter = int(self.PageCounter or 0) - nbpages
+        self.LifePageCounter = int(self.LifePageCounter or 0) - nbpages
+        
+        
 class StorageGroupPQuota(StorageObject) :
     """Group Print Quota class."""
@@ -427,4 +445,5 @@
         self.Exists = False
         self.isDirty = False
+        
         
 class StorageJob(StorageObject) :
@@ -462,4 +481,28 @@
         else :
             raise AttributeError, name
+            
+    def refund(self) :        
+        """Refund a particular print job."""
+        if (not self.JobSize) or (self.JobAction in ("DENY", "REFUND")) :
+            return
+        self.parent.beginTransaction()
+        try :
+            if self.JobBillingCode :
+                bcode = self.parent.getBillingCode(self.JobBillingCode)
+                bcode.refund(self.JobSize, self.JobPrice)
+                
+            if self.User.Exists :
+                self.User.refund(self.JobPrice)
+                if self.Printer.Exists :    
+                    upq = self.parent.getUserPQuota(self.User, self.Printer)    
+                    if upq.Exists :
+                        upq.refund(self.JobSize)
+            self.parent.refundJob(self.ident)
+        except :        
+            self.parent.rollbackTransaction()
+            raise
+        else :    
+            self.parent.commitTransaction()
+        
         
 class StorageLastJob(StorageJob) :
@@ -469,4 +512,5 @@
         self.PrinterName = printer.Name # not needed
         self.Printer = printer
+        
         
 class StorageBillingCode(StorageObject) :
@@ -494,7 +538,12 @@
         """Consumes some pages and credits for this billing code."""
         if pages :
-           self.parent.consumeBillingCode(self, pages, price)
-           self.PageCounter += pages
-           self.Balance -= price
+            self.parent.consumeBillingCode(self, pages, price)
+            self.PageCounter += pages
+            self.Balance -= price
+           
+    def refund(self, pages, price) :
+        """Refunds a particular billing code."""
+        self.consume(-pages, -price)
+        
         
 class BaseStorage :
Index: /pykota/trunk/cgi-bin/printquota.cgi
===================================================================
--- /pykota/trunk/cgi-bin/printquota.cgi (revision 3055)
+++ /pykota/trunk/cgi-bin/printquota.cgi (revision 3056)
@@ -210,5 +210,5 @@
                     billingcode = None
                 self.report = ["<h2>%s</h2>" % _("History")]    
-                history = self.storage.retrieveHistory(user, printer, hostname, billingcode, end=datelimit)
+                history = self.storage.retrieveHistory(user=user, printer=printer, hostname=hostname, billingcode=billingcode, end=datelimit)
                 if not history :
                     self.report.append("<h3>%s</h3>" % _("Empty"))
