Index: pykota/trunk/conf/pykota.conf.sample
===================================================================
--- pykota/trunk/conf/pykota.conf.sample (revision 2681)
+++ pykota/trunk/conf/pykota.conf.sample (revision 2692)
@@ -681,4 +681,14 @@
 # This option can only appear in the global section
 poorman: 2.0
+
+# The value of the zero for account balance limitations.
+# If an user his limited by balance, he can print until
+# his balance reaches the value defined here. If unset,
+# the default value is 0. Any floating point value
+# is accepted.
+#
+# This option can only appear in the global section
+# balancezero : -0.25
+balancezero: 0.0
 
 # Poor man's warning message
@@ -1003,2 +1013,10 @@
 denyduplicates : no
 
+# Sets the delay in seconds after which two identical jobs are 
+# not considered as being a duplicate.
+# 
+# This can be defined either globally or on a per printer basis
+# The default value if not set is 0, for 0 seconds.
+# duplicatesdelay : 300
+duplicatesdelay : 0
+
Index: pykota/trunk/pykota/tool.py
===================================================================
--- pykota/trunk/pykota/tool.py (revision 2657)
+++ pykota/trunk/pykota/tool.py (revision 2692)
@@ -458,5 +458,6 @@
             if enforcement == "STRICT" : 
                 val -= self.softwareJobPrice # use precomputed size.
-            if val <= 0.0 :
+            balancezero = self.config.getBalanceZero()
+            if val <= balancezero :
                 action = "DENY"
             elif val <= self.config.getPoorMan() :    
@@ -464,5 +465,5 @@
             else :    
                 action = "ALLOW"
-            if (enforcement == "STRICT") and (val == 0.0) :
+            if (enforcement == "STRICT") and (val == balancezero) :
                 action = "WARN" # we can still print until account is 0
         else :
@@ -549,5 +550,6 @@
                     if enforcement == "STRICT" : 
                         val -= self.softwareJobPrice # use precomputed size.
-                    if val <= 0.0 :
+                    balancezero = self.config.getBalanceZero()    
+                    if val <= balancezero :
                         action = "DENY"
                     elif val <= self.config.getPoorMan() :    
@@ -555,5 +557,5 @@
                     else :
                         action = "ALLOW"
-                    if (enforcement == "STRICT") and (val == 0.0) :
+                    if (enforcement == "STRICT") and (val == balancezero) :
                         action = "WARN" # we can still print until account is 0
                 return action    
Index: pykota/trunk/pykota/reporter.py
===================================================================
--- pykota/trunk/pykota/reporter.py (revision 2622)
+++ pykota/trunk/pykota/reporter.py (revision 2692)
@@ -100,5 +100,6 @@
         else :
             if entry.LimitBy.lower() == "balance" :
-                if balance == 0.0 :
+                balancezero = self.tool.config.getBalanceZero()
+                if balance == balancezero :
                     if entry.OverCharge > 0 :
                         datelimit = "DENY"
@@ -109,5 +110,5 @@
                         datelimit = ""
                         reached = "-B"
-                elif balance < 0 :
+                elif balance < balancezero :
                     datelimit = "DENY"
                     reached = "+B"
Index: pykota/trunk/pykota/config.py
===================================================================
--- pykota/trunk/pykota/config.py (revision 2635)
+++ pykota/trunk/pykota/config.py (revision 2692)
@@ -439,4 +439,15 @@
             raise PyKotaConfigError, _("Invalid poor man's threshold %s") % pm
             
+    def getBalanceZero(self) :    
+        """Returns the value of the zero for balance limitation."""
+        try :
+            bz = self.getGlobalOption("balancezero")
+        except PyKotaConfigError :    
+            bz = 0.0    # default value, zero is 0.0
+        try :
+            return float(bz)
+        except (TypeError, ValueError) :    
+            raise PyKotaConfigError, _("Invalid balancezero value %s") % bz
+            
     def getPoorWarn(self) :    
         """Returns the poor man's warning message."""
@@ -516,4 +527,16 @@
                 # it's a command to run.
                 return denyduplicates
+                
+    def getDuplicatesDelay(self, printername) :          
+        """Returns the number of seconds after which two identical jobs are not considered a duplicate anymore."""
+        try : 
+            duplicatesdelay = self.getPrinterOption(printername, "duplicatesdelay")
+        except PyKotaConfigError :    
+            return 0
+        else :    
+            try :
+                return int(duplicatesdelay)
+            except (TypeError, ValueError) :
+                raise PyKotaConfigError, _("Incorrect value %s for the duplicatesdelay directive in section %s") % (str(duplicatesdelay), printername)
         
     def getWinbindSeparator(self) :          
Index: pykota/trunk/pykota/version.py
===================================================================
--- pykota/trunk/pykota/version.py (revision 2668)
+++ pykota/trunk/pykota/version.py (revision 2692)
@@ -22,5 +22,5 @@
 #
 
-__version__ = "1.24alpha11_unofficial"
+__version__ = "1.24alpha12_unofficial"
 
 __doc__ = "PyKota : a complete Printing Quota Solution for CUPS."
Index: pykota/trunk/bin/warnpykota
===================================================================
--- pykota/trunk/bin/warnpykota (revision 2622)
+++ pykota/trunk/bin/warnpykota (revision 2692)
@@ -118,5 +118,6 @@
                     # we only want to warn users who have ever printed something
                     # and don't want to warn users who have never printed
-                    if (user.AccountBalance and (user.AccountBalance != user.LifeTimePaid)) or \
+                    if ((user.AccountBalance > self.config.getBalanceZero()) and \
+                       (user.AccountBalance != user.LifeTimePaid)) or \
                        userpquota.PageCounter or userpquota.LifePageCounter or \
                        self.storage.getUserNbJobsFromHistory(user) :
Index: pykota/trunk/bin/cupspykota
===================================================================
--- pykota/trunk/bin/cupspykota (revision 2687)
+++ pykota/trunk/bin/cupspykota (revision 2692)
@@ -40,4 +40,6 @@
 from email.MIMEText import MIMEText
 from email.Header import Header
+
+from mx import DateTime
 
 from pykota.tool import PyKotaTool, PyKotaToolError, crashed
@@ -768,30 +770,42 @@
         if not denyduplicates :
             self.logdebug("We don't care about duplicate jobs after all.")
-        elif self.Printer.LastJob.Exists \
-             and (self.Printer.LastJob.UserName == self.UserName) \
-             and (self.Printer.LastJob.JobMD5Sum == self.JobMD5Sum) :
-            # TODO : use the current user's last job instead of  
-            # TODO : the current printer's last job. This would be
-            # TODO : better but requires an additional database query
-            # TODO : with SQL, and is much more complex with the 
-            # TODO : actual LDAP schema. Maybe this is not very
-            # TODO : important, because usually duplicate jobs are sucessive.
-            msg = _("Job is a dupe")
-            if denyduplicates == 1 :
-                self.printInfo("%s : %s." % (msg, _("Printing is denied by configuration")), "warn")
-                self.Action = "DENY"
-                self.Reason = _("Duplicate print jobs are not allowed on printer %s.") % self.PrinterName
-            else :    
-                self.logdebug("Launching subprocess [%s] to see if duplicate jobs should be allowed or not." % denyduplicates)
-                fanswer = os.popen(denyduplicates, "r")
-                self.Action = fanswer.read().strip().upper()
-                fanswer.close()
-                if self.Action == "DENY" :     
-                    self.printInfo("%s : %s." % (msg, _("Subprocess denied printing of a dupe")), "warn")
-                    self.Reason = _("Duplicate print jobs are not allowed on printer %s at this time.") % self.PrinterName
-                else :    
-                    self.printInfo("%s : %s." % (msg, _("Subprocess allowed printing of a dupe")), "warn")
-        else :            
-            self.logdebug("Job doesn't seem to be a duplicate.")
+        else :
+            if self.Printer.LastJob.Exists \
+                    and (self.Printer.LastJob.UserName == self.UserName) \
+                    and (self.Printer.LastJob.JobMD5Sum == self.JobMD5Sum) :
+                now = DateTime.now()
+                try :
+                    previous = DateTime.ISO.ParseDateTime(self.Printer.LastJob.JobDate).localtime()
+                except :
+                    previous = now
+                difference = (now - previous).seconds
+                duplicatesdelay = self.config.getDuplicatesDelay(self.PrinterName)
+                self.logdebug("Difference with previous job : %.2f seconds. Duplicates delay : %.2f seconds." % (difference, duplicatesdelay))
+                if difference > duplicatesdelay :
+                    self.logdebug("Duplicate job allowed because previous one is more than %.2f seconds old." % duplicatesdelay)
+                else :
+                    # TODO : use the current user's last job instead of  
+                    # TODO : the current printer's last job. This would be
+                    # TODO : better but requires an additional database query
+                    # TODO : with SQL, and is much more complex with the 
+                    # TODO : actual LDAP schema. Maybe this is not very
+                    # TODO : important, because usually duplicate jobs are sucessive.
+                    msg = _("Job is a dupe")
+                    if denyduplicates == 1 :
+                        self.printInfo("%s : %s." % (msg, _("Printing is denied by configuration")), "warn")
+                        self.Action = "DENY"
+                        self.Reason = _("Duplicate print jobs are not allowed on printer %s.") % self.PrinterName
+                    else :    
+                        self.logdebug("Launching subprocess [%s] to see if duplicate jobs should be allowed or not." % denyduplicates)
+                        fanswer = os.popen(denyduplicates, "r")
+                        self.Action = fanswer.read().strip().upper()
+                        fanswer.close()
+                        if self.Action == "DENY" :     
+                            self.printInfo("%s : %s." % (msg, _("Subprocess denied printing of a dupe")), "warn")
+                            self.Reason = _("Duplicate print jobs are not allowed on printer %s at this time.") % self.PrinterName
+                        else :    
+                            self.printInfo("%s : %s." % (msg, _("Subprocess allowed printing of a dupe")), "warn")
+            else :            
+                self.logdebug("Job doesn't seem to be a duplicate.")
         self.logdebug("Checking if the job is a duplicate done.")
         
Index: pykota/trunk/bin/pykosd
===================================================================
--- pykota/trunk/bin/pykosd (revision 2622)
+++ pykota/trunk/bin/pykosd (revision 2692)
@@ -112,10 +112,10 @@
         while 1 :
             color = savecolor
-            user = cmd.storage.getUserFromBackend(uname)        # don't use cache
+            user = self.storage.getUserFromBackend(uname)        # don't use cache
             if not user.Exists :
                 raise PyKotaCommandLineError, _("User %s doesn't exist in PyKota's database") % uname
             if user.LimitBy == "quota" :    
-                printers = cmd.storage.getMatchingPrinters("*")
-                upquotas = [ cmd.storage.getUserPQuotaFromBackend(user, p) for p in printers ] # don't use cache
+                printers = self.storage.getMatchingPrinters("*")
+                upquotas = [ self.storage.getUserPQuotaFromBackend(user, p) for p in printers ] # don't use cache
                 nblines = len(upquotas)
                 display = pyosd.osd(font=options["font"], colour=color, timeout=duration, shadow=2, lines=nblines)
@@ -131,5 +131,5 @@
                     display.display(_("Pages used on %s : %s") % (upq.Printer.Name, percent), type=pyosd.TYPE_STRING, line=line)
             elif user.LimitBy == "balance" :
-                if user.AccountBalance <= 0 :
+                if user.AccountBalance <= self.config.getBalanceZero() :
                     color = "#FF0000"
                 display = pyosd.osd(font=options["font"], colour=color, timeout=duration, shadow=2)
Index: pykota/trunk/NEWS
===================================================================
--- pykota/trunk/NEWS (revision 2686)
+++ pykota/trunk/NEWS (revision 2692)
@@ -22,4 +22,14 @@
 PyKota NEWS :
        
+    - 1.24alpha12 :
+    
+        - Introduced the 'duplicatesdelay' directive to specify after
+          which duration two duplicate jobs are not considered duplicate
+          jobs anymore.
+          
+        - Introduced the 'balancezero' directive to specify any value  
+          as the balance limit instead of 0.0. This can allow admins to
+          give a few free credits to users, for example.
+          
     - 1.24alpha11 :
     
