Index: pykota/trunk/conf/pykota.conf.sample
===================================================================
--- pykota/trunk/conf/pykota.conf.sample (revision 2028)
+++ pykota/trunk/conf/pykota.conf.sample (revision 2054)
@@ -610,4 +610,8 @@
 #                         printers, or "localhost" if not defined or not
 #                         meaningful.
+# PYKOTAWARNCOUNT : the number of times the user was forbidden to print but a banner
+#                   page was still printed on the current printer.                   
+# PYKOTAOVERCHARGE : user's overcharging factor.
+#
 
 # PreHook : gets executed after being sure the user, printer and user quota
@@ -657,4 +661,15 @@
 # accountbanner: Both
 
+# Maximal number of times the banner will still be printed if
+# the user is forbidden to print.
+#
+# NB : CUPS ONLY FOR NOW !
+# 
+# This option can be set either globally or on a per printer basis.
+# Allowed values are 0 or any positive integer.
+# Default value is 0, which means that the banner won't be printed
+# at all if the user is forbidden to print.
+maxdenybanners: 0
+
 # StartingBanner : if defined will print a banner before the rest of the job 
 # is printed. The argument can be a printable file, or an executable file.
Index: pykota/trunk/bin/edpykota
===================================================================
--- pykota/trunk/bin/edpykota (revision 2036)
+++ pykota/trunk/bin/edpykota (revision 2054)
@@ -24,4 +24,11 @@
 #
 # $Log$
+# Revision 1.87  2005/02/13 22:02:28  jalet
+# Big database structure changes. Upgrade script is now included as well as
+# the new LDAP schema.
+# Introduction of the -o | --overcharge command line option to edpykota.
+# The output of repykota is more complete, but doesn't fit in 80 columns anymore.
+# Introduction of the new 'maxdenybanners' directive.
+#
 # Revision 1.86  2005/01/21 14:40:01  jalet
 # edpykota's --delete command line tool doesn't use "*" as its default argument
@@ -337,4 +344,14 @@
                        If both are to be set, separate them with a comma.
                        Floating point values are allowed.
+                       
+  -o | --overcharge f  Sets the overcharging factor applied to the user 
+                       when computing the cost of a print job. Positive or 
+                       negative floating point values are allowed,
+                       this allows you to do some really creative
+                       things like giving money to an user whenever
+                       he prints. The number of pages in a print job
+                       is not modified by this coefficient, only the
+                       cost of the job for a particular user.
+                       Only users have a coefficient.
   
   -i | --ingroups g1[,g2...]  Puts the users into each of the groups
@@ -480,4 +497,18 @@
   When printing either on hplj1 or hplj2, print quota will also be 
   checked and accounted for on virtual printers Laser and HP.
+  
+  $ edpykota --overcharge 2.5 poorstudent
+  
+  This will overcharge the poorstudent user by a factor of 2.5.
+  
+  $ edpykota --overcharge -1 jerome
+  
+  User jerome will actually earn money whenever he prints.
+  
+  $ edpykota --overcharge 0 boss
+  
+  User boss can print at will, it won't cost him anything because the
+  cost of each print job will be multiplied by zero before charging
+  his account.
 
 This program is free software; you can redistribute it and/or modify
@@ -532,4 +563,11 @@
                 (softlimit, hardlimit) = (hardlimit, softlimit)
             
+        overcharge = options["overcharge"]
+        if overcharge :
+            try :
+                overcharge = float(overcharge.strip())
+            except (ValueError, AttributeError) :    
+                raise PyKotaToolError, _("Invalid overcharge value %s") % options["overcharge"]
+                
         balance = options["balance"]
         if balance :
@@ -710,4 +748,9 @@
                             entrypquota.setUsage(used)
                             
+                        if overcharge is not None :    
+                            if changed[entry.Name].get("overcharge") is None :
+                                entry.setOverChargeFactor(overcharge)
+                                changed[entry.Name]["overcharge"] = overcharge
+                                
                         if balance :
                             if changed[entry.Name].get("balance") is None :
@@ -742,6 +785,6 @@
                      "printer" : "*", \
                    }
-        short_options = "vhdc:l:b:i:naugrp:P:S:H:G:RU:"
-        long_options = ["help", "version", "charge=", "delete", "limitby=", "balance=", "ingroups=", "noquota", "add", "users", "groups", "reset", "hardreset", "prototype=", "printer=", "softlimit=", "hardlimit=", "pgroups=", "used="]
+        short_options = "vhdo:c:l:b:i:naugrp:P:S:H:G:RU:"
+        long_options = ["help", "version", "overcharge=", "charge=", "delete", "limitby=", "balance=", "ingroups=", "noquota", "add", "users", "groups", "reset", "hardreset", "prototype=", "printer=", "softlimit=", "hardlimit=", "pgroups=", "used="]
         
         # Initializes the command line tool
@@ -771,4 +814,5 @@
         options["hardreset"] = options["R"] or options["hardreset"] 
         options["used"] = options["U"] or options["used"]
+        options["overcharge"] = options["o"] or options["overcharge"]
         
         if options["help"] :
@@ -784,5 +828,5 @@
         elif options["noquota"] and (options["prototype"] or options["hardlimit"] or options["softlimit"]) :
             raise PyKotaToolError, _("incompatible options, see help.")
-        elif options["groups"] and (options["balance"] or options["ingroups"] or options["used"]) :
+        elif options["groups"] and (options["balance"] or options["ingroups"] or options["used"] or options["overcharge"]) :
             raise PyKotaToolError, _("incompatible options, see help.")
         else :
Index: pykota/trunk/bin/cupspykota
===================================================================
--- pykota/trunk/bin/cupspykota (revision 2028)
+++ pykota/trunk/bin/cupspykota (revision 2054)
@@ -24,4 +24,11 @@
 #
 # $Log$
+# Revision 1.85  2005/02/13 22:02:28  jalet
+# Big database structure changes. Upgrade script is now included as well as
+# the new LDAP schema.
+# Introduction of the -o | --overcharge command line option to edpykota.
+# The output of repykota is more complete, but doesn't fit in 80 columns anymore.
+# Introduction of the new 'maxdenybanners' directive.
+#
 # Revision 1.84  2005/01/17 08:44:23  jalet
 # Modified copyright years
@@ -501,8 +508,14 @@
             accountbanner = self.config.getAccountBanner(printer.Name)
             if accountbanner in ["ENDING", "NONE"] :
-                banner = self.startingBanner(printer.Name)
-                if banner :
-                    self.logdebug("Printing starting banner before accounting begins.")
-                    self.handleData(banner)
+                if (action == 'DENY') and (userpquota.WarnCount < self.config.getMaxDenyBanners()) :
+                    self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), "warn")
+                else :
+                    if action == 'DENY' :
+                        userpquota.warn() # increments the warning counter
+                        self.exportUserInfo(userpquota)
+                    banner = self.startingBanner(printer.Name)
+                    if banner :
+                        self.logdebug("Printing starting banner before accounting begins.")
+                        self.handleData(banner)
  
             self.printMoreInfo(user, printer, _("Job accounting begins."))
@@ -511,10 +524,16 @@
             # handle starting banner pages during accounting
             if accountbanner in ["STARTING", "BOTH"] :
-                banner = self.startingBanner(printer.Name)
-                if banner :
-                    self.logdebug("Printing starting banner during accounting.")
-                    self.handleData(banner)
-                    if self.accounter.isSoftware :
-                        bannersize += 1 # TODO : fix this by passing the banner's content through PDLAnalyzer
+                if (action == 'DENY') and (userpquota.WarnCount < self.config.getMaxDenyBanners()) :
+                    self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), "warn")
+                else :
+                    if action == 'DENY' :
+                        userpquota.warn() # increments the warning counter
+                        self.exportUserInfo(userpquota)
+                    banner = self.startingBanner(printer.Name)
+                    if banner :
+                        self.logdebug("Printing starting banner during accounting.")
+                        self.handleData(banner)
+                        if self.accounter.isSoftware :
+                            bannersize += 1 # TODO : fix this by passing the banner's content through PDLAnalyzer
         else :    
             action = "ALLOW"
@@ -536,10 +555,16 @@
             # handle ending banner pages during accounting
             if accountbanner in ["ENDING", "BOTH"] :
-                banner = self.endingBanner(printer.Name)
-                if banner :
-                    self.logdebug("Printing ending banner during accounting.")
-                    self.handleData(banner)
-                    if self.accounter.isSoftware :
-                        bannersize += 1 # TODO : fix this by passing the banner's content through PDLAnalyzer
+                if (action == 'DENY') and (userpquota.WarnCount < self.config.getMaxDenyBanners()) :
+                    self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), "warn")
+                else :
+                    if action == 'DENY' :
+                        userpquota.warn() # increments the warning counter
+                        self.exportUserInfo(userpquota)
+                    banner = self.endingBanner(printer.Name)
+                    if banner :
+                        self.logdebug("Printing ending banner during accounting.")
+                        self.handleData(banner)
+                        if self.accounter.isSoftware :
+                            bannersize += 1 # TODO : fix this by passing the banner's content through PDLAnalyzer
  
             # stops accounting. 
@@ -575,9 +600,15 @@
             # handle ending banner pages after accounting ends
             if accountbanner in ["STARTING", "NONE"] :
-                banner = self.endingBanner(printer.Name)
-                if banner :
-                    self.logdebug("Printing ending banner after accounting ends.")
-                    self.handleData(banner)
-            
+                if (action == 'DENY') and (userpquota.WarnCount < self.config.getMaxDenyBanners()) :
+                    self.printInfo(_("Banner won't be printed : maximum number of deny banners reached."), "warn")
+                else :
+                    if action == 'DENY' :
+                        userpquota.warn() # increments the warning counter
+                        self.exportUserInfo(userpquota)
+                    banner = self.endingBanner(printer.Name)
+                    if banner :
+                        self.logdebug("Printing ending banner after accounting ends.")
+                        self.handleData(banner)
+                        
             # Launches the post hook
             self.posthook(userpquota)
@@ -823,5 +854,5 @@
             try :
                 status = subprocess.wait()
-            except OSError : # already dead    
+            except OSError : # already dead : TODO : detect when abnormal
                 status = 0
         if os.WIFEXITED(status) :
Index: pykota/trunk/NEWS
===================================================================
--- pykota/trunk/NEWS (revision 2044)
+++ pykota/trunk/NEWS (revision 2054)
@@ -22,4 +22,25 @@
 PyKota NEWS :
        
+    - 1.21alpha24 :
+    
+        - Big database structure changes. An upgrade script
+          is now included for PostgreSQL. LDAP users may just
+          want to copy the new schema in place.
+          
+        - edpykota now recognizes the -o | --overcharge command line
+          option to set an overcharging or undercharging factor
+          on each print job's cost for a particular user.
+          
+        - repykota's output format has changed an is not limited to  
+          80 characters wide anymore. People who used to parse 
+          repykota's output in their own tools are strongly advised
+          to parse dumpykota's output instead.
+          
+        - Introduction of the new 'maxdenybanners' configuration directive
+          to allow up to N (the directive's value) deny banners to be
+          printed even if the print account is forbidden to print.
+          
+        - Better detection of the number of copies in the PostScript parser.
+        
     - 1.21alpha23 :
     
Index: pykota/trunk/po/zh_TW/pykota.po
===================================================================
--- pykota/trunk/po/zh_TW/pykota.po (revision 2043)
+++ pykota/trunk/po/zh_TW/pykota.po (revision 2054)
@@ -28,5 +28,5 @@
 "Project-Id-Version: PyKota v1.20\n"
 "Report-Msgid-Bugs-To: alet@librelogiciel.com\n"
-"POT-Creation-Date: 2005-01-25 09:03+0800\n"
+"POT-Creation-Date: 2004-11-25 10:16+0100\n"
 "PO-Revision-Date: 2005-01-25 09:03+0800\n"
 "Last-Translator: Tsang Yu Pong, Teddy, Wing Kwong College in Hong Kong,"
@@ -39,5 +39,6 @@
 #, python-format
 msgid ""
-"pkbanner v%s (c) 2003, 2004, 2005 C@LL - Conseil Internet & Logiciels Libres\n"
+"pkbanner v%s (c) 2003, 2004, 2005 C@LL - Conseil Internet & Logiciels "
+"Libres\n"
 "\n"
 "Generates banners.\n"
@@ -292,5 +293,6 @@
 #, python-format
 msgid ""
-"pkprinters v%s (c) 2003, 2004, 2005 C@LL - Conseil Internet & Logiciels Libres\n"
+"pkprinters v%s (c) 2003, 2004, 2005 C@LL - Conseil Internet & Logiciels "
+"Libres\n"
 "A Printers Manager for PyKota.\n"
 "\n"
@@ -360,5 +362,5 @@
 "  $ pkprinters --delete \"*\"\n"
 "  \n"
-"This will completely delete all printers and associated quota "
+"  This will completely delete all printers and associated quota "
 "information,\n"
 "  as well as their job history. USE WITH CARE !\n"
@@ -589,5 +591,6 @@
 #, python-format
 msgid ""
-"autopykota v%s (c) 2003, 2004, 2005 C@LL - Conseil Internet & Logiciels Libres\n"
+"autopykota v%s (c) 2003, 2004, 2005 C@LL - Conseil Internet & Logiciels "
+"Libres\n"
 "A tool to automate user account creation and initial balance setting.\n"
 "\n"
@@ -664,5 +667,6 @@
 #, python-format
 msgid ""
-"dumpykota v%s (c) 2003, 2004, 2005 C@LL - Conseil Internet & Logiciels Libres\n"
+"dumpykota v%s (c) 2003, 2004, 2005 C@LL - Conseil Internet & Logiciels "
+"Libres\n"
 "\n"
 "Dumps PyKota database's content.\n"
@@ -757,5 +761,6 @@
 #, python-format
 msgid ""
-"edpykota v%s (c) 2003, 2004, 2005 C@LL - Conseil Internet & Logiciels Libres\n"
+"edpykota v%s (c) 2003, 2004, 2005 C@LL - Conseil Internet & Logiciels "
+"Libres\n"
 "A Print Quota editor for PyKota.\n"
 "\n"
@@ -1014,5 +1019,6 @@
 #, python-format
 msgid ""
-"repykota v%s (c) 2003, 2004, 2005 C@LL - Conseil Internet & Logiciels Libres\n"
+"repykota v%s (c) 2003, 2004, 2005 C@LL - Conseil Internet & Logiciels "
+"Libres\n"
 "\n"
 "Generates print quota reports.\n"
@@ -1078,5 +1084,6 @@
 #, python-format
 msgid ""
-"warnpykota v%s (c) 2003, 2004, 2005 C@LL - Conseil Internet & Logiciels Libres\n"
+"warnpykota v%s (c) 2003, 2004, 2005 C@LL - Conseil Internet & Logiciels "
+"Libres\n"
 "\n"
 "Sends mail to users over print quota.\n"
@@ -1255,7 +1262,6 @@
 "paid"
 msgstr ""
-"群組           已列印    警告限制    有效限制    結餘 寬限日期        付款總計       "
-"已繳付"
-
+"群組           已列印    警告限制    有效限制    結餘 寬限日期        付款總"
+"計       已繳付"
 
 msgid ""
@@ -1263,6 +1269,6 @@
 "paid"
 msgstr ""
-"群組           已列印    警告限制    有效限制    結餘 寬限日期         付款總計       "
-"已繳付"
+"群組           已列印    警告限制    有效限制    結餘 寬限日期         付款總"
+"計       已繳付"
 
 msgid "unknown"
@@ -1314,7 +1320,5 @@
 "Unable to find user %s's account balance, applying default policy (%s) for "
 "printer %s"
-msgstr ""
-"未能尋找使用者 %s 的帳目結算，因此採用預設的政策(%s)於"
-"列印機 %s"
+msgstr "未能尋找使用者 %s 的帳目結算，因此採用預設的政策(%s)於列印機 %s"
 
 #, python-format
@@ -1336,6 +1340,4 @@
 msgstr "使用者 %s 的列印配額於列印機 %s 經已超出配額"
 
-#, python
-
 #, python-format
 msgid "Print Quota low for user %s on printer %s"
@@ -1345,5 +1347,4 @@
 msgstr "列印配額經已很低"
 
-
 #, python-format
 msgid "Printing system %s, args=%s"
@@ -1374,6 +1375,5 @@
 "s) for printer %s"
 msgstr ""
-"列印機 %s 在PyKota系統內沒有登記，因此這台列印機"
-"套用external policy (%s) "
+"列印機 %s 在PyKota系統內沒有登記，因此這台列印機套用external policy (%s) "
 
 #, python-format
@@ -1382,6 +1382,6 @@
 "for printer %s"
 msgstr ""
-"使用者 %s 在PyKota內沒有登記，因此這個使用者將會套用external policy(%s) "
-"於列印機 %s "
+"使用者 %s 在PyKota內沒有登記，因此這個使用者將會套用external policy(%s) 於列"
+"印機 %s "
 
 #, python-format
@@ -1389,7 +1389,5 @@
 "User %s doesn't have quota on printer %s in the PyKota system, applying "
 "external policy (%s) for printer %s"
-msgstr ""
-"使用者沒有列印配額於列印機 %s ，套用對外政策 (%s)"
-" 於列印機 %s"
+msgstr "使用者沒有列印配額於列印機 %s ，套用對外政策 (%s) 於列印機 %s"
 
 #, python-format
@@ -1397,12 +1395,10 @@
 "External policy %s for printer %s produced an error. Job rejected. Please "
 "check PyKota's configuration files."
-msgstr ""
-"對外政策 %s 於列印機 %s 產生錯誤，列印工作被拒，請撿查PyKota的設定檔案"
+msgstr "對外政策 %s 於列印機 %s 產生錯誤，列印工作被拒，請撿查PyKota的設定檔案"
 
 #, python-format
 msgid ""
 "Printer %s not registered in the PyKota system, applying default policy (%s)"
-msgstr ""
-"列印機 %s 沒有於PyKota系統內登記，套用預設的政策 (%s)"
+msgstr "列印機 %s 沒有於PyKota系統內登記，套用預設的政策 (%s)"
 
 #, python-format
@@ -1410,7 +1406,5 @@
 "User %s not registered in the PyKota system, applying default policy (%s) "
 "for printer %s"
-msgstr ""
-"使用者 %s 沒有於PyKota系統內登記，套用預設的政策 (%s)"
-"於列印機 %s"
+msgstr "使用者 %s 沒有於PyKota系統內登記，套用預設的政策 (%s)於列印機 %s"
 
 #, python-format
@@ -1418,12 +1412,10 @@
 "User %s doesn't have quota on printer %s in the PyKota system, applying "
 "default policy (%s)"
-msgstr ""
-"使用者 %s 沒有列印配額於列印機 %s，套用預設的政策 (%s)"
+msgstr "使用者 %s 沒有列印配額於列印機 %s，套用預設的政策 (%s)"
 
 #, python-format
 msgid ""
 "Printer %s still not registered in the PyKota system, job will be rejected"
-msgstr ""
-"列印機 %s 依然沒有於PyKota系統內登記，列印工作要求被拒"
+msgstr "列印機 %s 依然沒有於PyKota系統內登記，列印工作要求被拒"
 
 #, python-format
@@ -1431,7 +1423,5 @@
 "User %s still not registered in the PyKota system, job will be rejected on "
 "printer %s"
-msgstr ""
-"使用者 %s 依然沒有於PyKota系統內登記，列印工作要求被拒於"
-"列印機 %s"
+msgstr "使用者 %s 依然沒有於PyKota系統內登記，列印工作要求被拒於列印機 %s"
 
 #, python-format
@@ -1439,6 +1429,5 @@
 "User %s still doesn't have quota on printer %s in the PyKota system, job "
 "will be rejected"
-msgstr ""
-"使用者 %s 依然沒有列印配額於列印機 %s 在PyKota系統，列印工作被拒"
+msgstr "使用者 %s 依然沒有列印配額於列印機 %s 在PyKota系統，列印工作被拒"
 
 #, python-format
@@ -1482,6 +1471,5 @@
 "Couldn't retrieve printer %s's internal page counter either before or after "
 "printing."
-msgstr ""
-"不能恢復列印機 %s 的內存頁面計算器不論在列印的之前或之後"
+msgstr "不能恢復列印機 %s 的內存頁面計算器不論在列印的之前或之後"
 
 #, python-format
@@ -1501,6 +1489,6 @@
 "download it from http://pysnmp.sourceforge.net"
 msgstr ""
-"要求內存的SNMP計算器，但找不到Python-SNMP"
-"請到http://pysnmp.sourceforge.net下載"
+"要求內存的SNMP計算器，但找不到Python-SNMP請到http://pysnmp.sourceforge.net下"
+"載"
 
 #, python-format
@@ -1532,7 +1520,5 @@
 "SNMP querying stage interrupted. Using latest value seen for internal page "
 "counter (%s) on printer %s."
-msgstr ""
-"SNMP查詢被中斷，使用最更新的數值來計算頁數"
-"計算器 (%s) 在列印機 %s 上"
+msgstr "SNMP查詢被中斷，使用最更新的數值來計算頁數計算器 (%s) 在列印機 %s 上"
 
 #, python-format
@@ -1540,7 +1526,5 @@
 "PJL querying stage interrupted. Using latest value seen for internal page "
 "counter (%s) on printer %s."
-msgstr ""
-"PJL查詢被中斷，使用最更新的數值來計算頁數"
-"計算器 (%s) 在列印機 %s 上"
+msgstr "PJL查詢被中斷，使用最更新的數值來計算頁數計算器 (%s) 在列印機 %s 上"
 
 #, python-format
@@ -1567,7 +1551,5 @@
 "Search base %s doesn't seem to exist. Probable misconfiguration. Please "
 "double check /etc/pykota/pykota.conf : %s"
-msgstr ""
-"%s 不存在，很可能是錯誤設定所致"
-"請撿查 /etc/pykota/pykota.conf : %s"
+msgstr "%s 不存在，很可能是錯誤設定所致請撿查 /etc/pykota/pykota.conf : %s"
 
 #, python-format
@@ -1591,5 +1573,7 @@
 "No pykotaAccountBalance object found for user %s. Did you create LDAP "
 "entries manually ?"
-msgstr "未能為使用者 %s 尋找pkotaAccountBalance object，請問你是否已手動建立LDAP 紀錄?  "
+msgstr ""
+"未能為使用者 %s 尋找pkotaAccountBalance object，請問你是否已手動建立LDAP 紀"
+"錄?  "
 
 #, python-format
@@ -1597,6 +1581,6 @@
 "Unable to find an existing objectClass %s entry with %s=%s to attach "
 "pykotaAccount objectClass"
-msgstr "未能尋找現存的objectclass %s 紀錄帶有 %s=%s"
-" 連繫到pykotaAccount objectClass "
+msgstr ""
+"未能尋找現存的objectclass %s 紀錄帶有 %s=%s 連繫到pykotaAccount objectClass "
 
 #, python-format
Index: pykota/trunk/po/fr_FR/pykota.po
===================================================================
--- pykota/trunk/po/fr_FR/pykota.po (revision 2028)
+++ pykota/trunk/po/fr_FR/pykota.po (revision 2054)
@@ -1120,5 +1120,5 @@
 msgid "Invalid modifier [%s] for --data command line option, see help."
 msgstr ""
-"Le modifier [%s] pour l'option de ligne de commande --data est invalide, "
+"Le modificateur [%s] pour l'option de ligne de commande --data est invalide, "
 "voir l'aide."
 
@@ -1126,6 +1126,6 @@
 msgid "Invalid modifier [%s] for --format command line option, see help."
 msgstr ""
-"Le modifier [%s] pour l'option de ligne de commande --format est invalide, "
-"voir l'aide."
+"Le modificateur [%s] pour l'option de ligne de commande --format est "
+"invalide, voir l'aide."
 
 msgid "XML output is disabled because the jaxml module is not available."
Index: pykota/trunk/README
===================================================================
--- pykota/trunk/README (revision 2051)
+++ pykota/trunk/README (revision 2054)
@@ -250,5 +250,5 @@
   You may learn more about PyKota, if it fits your own organization,
   its internal working, and some potential performance drawbacks and
-  how to avoid them, in a WiKi administered by Ryan Suarez at :
+  how to avoid them, in a document created by Ryan Suarez at :
   
     http://archive.macosxlabs.org/forum/webcrossing_archive/documentation/Pykota_and_CUPS/Pykota_and_CUPS.html
@@ -610,5 +610,5 @@
     
     which will print quota usage for all users on all printers,
-    along with totals, if you are the root user. If you are
+    along with totals, if you are a PyKota Administator. If you are
     a regular user, only your own quota report will be produced.
     
@@ -631,5 +631,5 @@
 
   - Kanakorn Horsiritham developped phpPykotaAdmin which is 
-    a web based administrative GUI :
+    a web based database independant administrative GUI :
   
       http://opensource.psu.ac.th/~kanakorn/mambo/
Index: pykota/trunk/TODO
===================================================================
--- pykota/trunk/TODO (revision 2028)
+++ pykota/trunk/TODO (revision 2054)
@@ -36,5 +36,5 @@
         - Better --prototype option in edpykota
         
-        - Web enabled pykotme and dumpykota.
+        - Web enabled pykotme.
         
         - Add an MD5 checksum of the job's datas to the
@@ -47,7 +47,4 @@
         - Maybe put "gracedelay" in the database.
         
-        - Price multiplier/divisor which can be set on a per
-          user or per user group basis.
-
         - Ink accounting ala PrintBill.
         
@@ -92,5 +89,6 @@
         - Documentation...
           
-        - Complete web administrative interface with graphical reports.
+        - Learn more bits of PHP to help with phpPyKotaAdmin, which
+          is a GREAT tool !
        
         - Group administrators (think quotagrpdmins for disk quotas).
Index: pykota/trunk/pykota/reporters/html.py
===================================================================
--- pykota/trunk/pykota/reporters/html.py (revision 1278)
+++ pykota/trunk/pykota/reporters/html.py (revision 2054)
@@ -22,4 +22,11 @@
 #
 # $Log$
+# Revision 1.9  2005/02/13 22:02:29  jalet
+# Big database structure changes. Upgrade script is now included as well as
+# the new LDAP schema.
+# Introduction of the -o | --overcharge command line option to edpykota.
+# The output of repykota is more complete, but doesn't fit in 80 columns anymore.
+# Introduction of the new 'maxdenybanners' directive.
+#
 # Revision 1.8  2004/01/12 15:28:45  jalet
 # Now can output the user's history on several printers at the same time.
@@ -81,5 +88,5 @@
                 else :    
                     oddevenclass = "even"
-                (pages, money, name, reached, pagecounter, soft, hard, balance, datelimit, lifepagecounter, lifetimepaid) = self.getQuota(entry, entrypquota)
+                (pages, money, name, reached, pagecounter, soft, hard, balance, datelimit, lifepagecounter, lifetimepaid, overcharge, warncount) = self.getQuota(entry, entrypquota)
                 if datelimit :
                     if datelimit == "DENY" :
@@ -89,5 +96,5 @@
                 if (not self.tool.config.getDisableHistory()) and (not self.isgroup) :
                     name = '<a href="%s?username=%s&printername=%s&history=1">%s</a>' % (os.environ.get("SCRIPT_NAME", ""), name, printer.Name, name)
-                self.report.append('<tr class="%s">%s</tr>' % (oddevenclass, "".join(["<td>%s</td>" % h for h in (name, reached, pagecounter, soft, hard, balance, datelimit or "&nbsp;", lifepagecounter, lifetimepaid)])))
+                self.report.append('<tr class="%s">%s</tr>' % (oddevenclass, "".join(["<td>%s</td>" % h for h in (name, reached, overcharge, pagecounter, soft, hard, balance, datelimit or "&nbsp;", lifepagecounter, lifetimepaid, warncount)])))
                 total += pages
                 totalmoney += money
@@ -95,6 +102,6 @@
             if total or totalmoney :        
                 (tpage, tmoney) = self.getTotals(total, totalmoney)
-                self.report.append('<tr class="totals"><td colspan="7">&nbsp;</td><td align="right">%s</td><td align="right">%s</td></tr>' % (tpage, tmoney))
-            self.report.append('<tr class="realpagecounter"><td colspan="7">&nbsp;</td><td align="right">%s</td></tr>' % self.getPrinterRealPageCounter(printer))
+                self.report.append('<tr class="totals"><td colspan="8">&nbsp;</td><td align="right">%s</td><td align="right">%s</td><td>&nbsp;</td></tr>' % (tpage, tmoney))
+            self.report.append('<tr class="realpagecounter"><td colspan="8">&nbsp;</td><td align="right">%s</td><td>&nbsp;</td></tr>' % self.getPrinterRealPageCounter(printer))
             self.report.append('</table>')
         if self.isgroup :    
Index: pykota/trunk/pykota/reporters/text.py
===================================================================
--- pykota/trunk/pykota/reporters/text.py (revision 1257)
+++ pykota/trunk/pykota/reporters/text.py (revision 2054)
@@ -22,4 +22,11 @@
 #
 # $Log$
+# Revision 1.10  2005/02/13 22:02:29  jalet
+# Big database structure changes. Upgrade script is now included as well as
+# the new LDAP schema.
+# Introduction of the -o | --overcharge command line option to edpykota.
+# The output of repykota is more complete, but doesn't fit in 80 columns anymore.
+# Introduction of the new 'maxdenybanners' directive.
+#
 # Revision 1.9  2004/01/08 14:10:33  jalet
 # Copyright year changed.
@@ -78,6 +85,6 @@
             self.report.append('-' * len(header))
             for (entry, entrypquota) in getattr(self.tool.storage, "getPrinter%ssAndQuotas" % prefix)(printer, self.ugnames) :
-                (pages, money, name, reached, pagecounter, soft, hard, balance, datelimit, lifepagecounter, lifetimepaid) = self.getQuota(entry, entrypquota)
-                self.report.append("%-9.9s %s %7i %7s %7s %10s %-10.10s %8i %10s" % (name, reached, pagecounter, soft, hard, balance, datelimit, lifepagecounter, lifetimepaid))
+                (pages, money, name, reached, pagecounter, soft, hard, balance, datelimit, lifepagecounter, lifetimepaid, overcharge, warncount) = self.getQuota(entry, entrypquota)
+                self.report.append("%-15.15s %s %5s %7i %7s %7s %10s %-10.10s %8i %10s %4s" % (name, reached, overcharge, pagecounter, soft, hard, balance, datelimit, lifepagecounter, lifetimepaid, warncount))
                 total += pages
                 totalmoney += money
@@ -85,6 +92,6 @@
             if total or totalmoney :        
                 (tpage, tmoney) = self.getTotals(total, totalmoney)
-                self.report.append((" " * 50) + tpage + tmoney)
-            self.report.append((" " * 51) + self.getPrinterRealPageCounter(printer))
+                self.report.append((" " * 62) + tpage + tmoney)
+            self.report.append((" " * 63) + self.getPrinterRealPageCounter(printer))
             self.report.append("")        
         if self.isgroup :    
Index: pykota/trunk/pykota/storages/sql.py
===================================================================
--- pykota/trunk/pykota/storages/sql.py (revision 2030)
+++ pykota/trunk/pykota/storages/sql.py (revision 2054)
@@ -22,4 +22,11 @@
 #
 # $Log$
+# Revision 1.65  2005/02/13 22:02:29  jalet
+# Big database structure changes. Upgrade script is now included as well as
+# the new LDAP schema.
+# Introduction of the -o | --overcharge command line option to edpykota.
+# The output of repykota is more complete, but doesn't fit in 80 columns anymore.
+# Introduction of the new 'maxdenybanners' directive.
+#
 # Revision 1.64  2005/01/18 19:47:50  jalet
 # Big bug fix wrt the datelimit attribute
@@ -273,4 +280,5 @@
             user.LifeTimePaid = fields.get("lifetimepaid")
             user.Email = fields.get("email")
+            user.OverCharge = fields.get("overcharge", 1.0)
             user.Exists = 1
         return user
@@ -317,4 +325,5 @@
                 userpquota.HardLimit = fields.get("hardlimit")
                 userpquota.DateLimit = fields.get("datelimit")
+                userpquota.WarnCount = fields.get("warncount")
                 userpquota.Exists = 1
         return userpquota
@@ -359,4 +368,7 @@
             lastjob.JobHostName = fields.get("hostname")
             lastjob.JobSizeBytes = fields.get("jobsizebytes")
+            lastjob.JobMD5Sum = fields.get("md5sum")
+            lastjob.JobPages = fields.get("pages")
+            lastjob.JobBillingCode = fields.get("billingcode")
             lastjob.Exists = 1
         return lastjob
@@ -374,4 +386,5 @@
                 user.LifeTimePaid = record.get("lifetimepaid")
                 user.Email = record.get("email")
+                user.OverCharge = record.get("overcharge")
                 user.Exists = 1
                 groupmembers.append(user)
@@ -423,5 +436,5 @@
         """Returns the list of users who uses a given printer, along with their quotas."""
         usersandquotas = []
-        result = self.doSearch("SELECT users.id as uid,username,balance,lifetimepaid,limitby,email,userpquota.id,lifepagecounter,pagecounter,softlimit,hardlimit,datelimit FROM users JOIN userpquota ON users.id=userpquota.userid AND printerid=%s ORDER BY username ASC" % self.doQuote(printer.ident))
+        result = self.doSearch("SELECT users.id as uid,username,balance,lifetimepaid,limitby,email,overcharge,userpquota.id,lifepagecounter,pagecounter,softlimit,hardlimit,datelimit,warncount FROM users JOIN userpquota ON users.id=userpquota.userid AND printerid=%s ORDER BY username ASC" % self.doQuote(printer.ident))
         if result :
             for record in result :
@@ -433,4 +446,5 @@
                     user.LifeTimePaid = record.get("lifetimepaid")
                     user.Email = record.get("email") 
+                    user.OverCharge = record.get("overcharge")
                     user.Exists = 1
                     userpquota = StorageUserPQuota(self, user, printer)
@@ -441,4 +455,5 @@
                     userpquota.HardLimit = record.get("hardlimit")
                     userpquota.DateLimit = record.get("datelimit")
+                    userpquota.WarnCount = record.get("warncount")
                     userpquota.Exists = 1
                     usersandquotas.append((user, userpquota))
@@ -466,5 +481,5 @@
     def addUser(self, user) :        
         """Adds a user to the quota storage, returns its id."""
-        self.doModify("INSERT INTO users (username, limitby, balance, lifetimepaid, email) VALUES (%s, %s, %s, %s, %s)" % (self.doQuote(user.Name), self.doQuote(user.LimitBy or 'quota'), self.doQuote(user.AccountBalance or 0.0), self.doQuote(user.LifeTimePaid or 0.0), self.doQuote(user.Email)))
+        self.doModify("INSERT INTO users (username, limitby, balance, lifetimepaid, email, overcharge) VALUES (%s, %s, %s, %s, %s, %s)" % (self.doQuote(user.Name), self.doQuote(user.LimitBy or 'quota'), self.doQuote(user.AccountBalance or 0.0), self.doQuote(user.LifeTimePaid or 0.0), self.doQuote(user.Email), self.doQuote(user.OverCharge)))
         return self.getUser(user.Name)
         
@@ -503,4 +518,8 @@
         self.doModify("UPDATE printers SET description=%s WHERE id=%s" % (self.doQuote(description), self.doQuote(printer.ident)))
         
+    def writeUserOverCharge(self, user, factor) :
+        """Sets the user's overcharging coefficient."""
+        self.doModify("UPDATE users SET overcharge=%s WHERE id=%s" % (self.doQuote(factor), self.doQuote(user.ident)))
+        
     def writeUserLimitBy(self, user, limitby) :    
         """Sets the user's limiting factor."""
@@ -521,13 +540,13 @@
     def increaseUserPQuotaPagesCounters(self, userpquota, nbpages) :    
         """Increase page counters for a user print quota."""
-        self.doModify("UPDATE userpquota SET pagecounter=pagecounter+%s,lifepagecounter=lifepagecounter+%s WHERE id=%s" % (self.doQuote(nbpages), self.doQuote(nbpages), self.doQuote(userpquota.ident)))
+        self.doModify("UPDATE userpquota SET pagecounter=pagecounter + %s,lifepagecounter=lifepagecounter + %s WHERE id=%s" % (self.doQuote(nbpages), self.doQuote(nbpages), self.doQuote(userpquota.ident)))
        
     def writeUserPQuotaPagesCounters(self, userpquota, newpagecounter, newlifepagecounter) :    
         """Sets the new page counters permanently for a user print quota."""
-        self.doModify("UPDATE userpquota SET pagecounter=%s, lifepagecounter=%s, datelimit=NULL WHERE id=%s" % (self.doQuote(newpagecounter), self.doQuote(newlifepagecounter), self.doQuote(userpquota.ident)))
+        self.doModify("UPDATE userpquota SET pagecounter=%s, lifepagecounter=%s, warncount=0, datelimit=NULL WHERE id=%s" % (self.doQuote(newpagecounter), self.doQuote(newlifepagecounter), self.doQuote(userpquota.ident)))
        
     def decreaseUserAccountBalance(self, user, amount) :    
         """Decreases user's account balance from an amount."""
-        self.doModify("UPDATE users SET balance=balance-%s WHERE id=%s" % (self.doQuote(amount), self.doQuote(user.ident)))
+        self.doModify("UPDATE users SET balance=balance - %s WHERE id=%s" % (self.doQuote(amount), self.doQuote(user.ident)))
        
     def writeUserAccountBalance(self, user, newbalance, newlifetimepaid=None) :    
@@ -565,5 +584,13 @@
     def writeUserPQuotaLimits(self, userpquota, softlimit, hardlimit) :
         """Sets soft and hard limits for a user quota."""
-        self.doModify("UPDATE userpquota SET softlimit=%s, hardlimit=%s, datelimit=NULL WHERE id=%s" % (self.doQuote(softlimit), self.doQuote(hardlimit), self.doQuote(userpquota.ident)))
+        self.doModify("UPDATE userpquota SET softlimit=%s, hardlimit=%s, warncount=0, datelimit=NULL WHERE id=%s" % (self.doQuote(softlimit), self.doQuote(hardlimit), self.doQuote(userpquota.ident)))
+        
+    def writeUserPQuotaWarnCount(self, userpquota, warncount) :
+        """Sets the warn counter value for a user quota."""
+        self.doModify("UPDATE userpquota SET warncount=%s WHERE id=%s" % (self.doQuote(warncount), self.doQuote(userpquota.ident)))
+        
+    def increaseUserPQuotaWarnCount(self, userpquota) :
+        """Increases the warn counter value for a user quota."""
+        self.doModify("UPDATE userpquota SET warncount=warncount+1 WHERE id=%s" % self.doQuote(userpquota.ident))
         
     def writeGroupPQuotaLimits(self, grouppquota, softlimit, hardlimit) :
@@ -620,4 +647,7 @@
                 job.JobHostName = fields.get("hostname")
                 job.JobSizeBytes = fields.get("jobsizebytes")
+                job.JobMD5Sum = fields.get("md5sum")
+                job.JobPages = fields.get("pages")
+                job.JobBillingCode = fields.get("billingcode")
                 job.UserName = fields.get("username")
                 job.PrinterName = fields.get("printername")
Index: pykota/trunk/pykota/storages/ldapstorage.py
===================================================================
--- pykota/trunk/pykota/storages/ldapstorage.py (revision 2042)
+++ pykota/trunk/pykota/storages/ldapstorage.py (revision 2054)
@@ -22,4 +22,11 @@
 #
 # $Log$
+# Revision 1.100  2005/02/13 22:02:29  jalet
+# Big database structure changes. Upgrade script is now included as well as
+# the new LDAP schema.
+# Introduction of the -o | --overcharge command line option to edpykota.
+# The output of repykota is more complete, but doesn't fit in 80 columns anymore.
+# Introduction of the new 'maxdenybanners' directive.
+#
 # Revision 1.99  2005/01/24 17:44:17  jalet
 # Same fix for group print quota entries wrt LDAP performance
@@ -647,17 +654,12 @@
         """Extracts user information given its name."""
         user = StorageUser(self, username)
-        result = self.doSearch("(&(objectClass=pykotaAccount)(|(pykotaUserName=%s)(%s=%s)))" % (username, self.info["userrdn"], username), ["pykotaUserName", "pykotaLimitBy", self.info["usermail"]], base=self.info["userbase"])
+        result = self.doSearch("(&(objectClass=pykotaAccount)(|(pykotaUserName=%s)(%s=%s)))" % (username, self.info["userrdn"], username), ["pykotaUserName", "pykotaLimitBy", self.info["usermail"], "pykotaOverCharge"], base=self.info["userbase"])
         if result :
             fields = result[0][1]
             user.ident = result[0][0]
             user.Name = fields.get("pykotaUserName", [username])[0] 
-            user.Email = fields.get(self.info["usermail"])
-            if user.Email is not None :
-                user.Email = user.Email[0]
-            user.LimitBy = fields.get("pykotaLimitBy")
-            if user.LimitBy is not None :
-                user.LimitBy = user.LimitBy[0]
-            else :    
-                user.LimitBy = "quota"
+            user.Email = fields.get(self.info["usermail"], [None])[0]
+            user.LimitBy = fields.get("pykotaLimitBy", ["quota"])[0]
+            user.OverCharge = float(fields.get("pykotaOverCharge", [1.0])[0])
             result = self.doSearch("(&(objectClass=pykotaAccountBalance)(|(pykotaUserName=%s)(%s=%s)))" % (username, self.info["balancerdn"], username), ["pykotaBalance", "pykotaLifeTimePaid", "pykotaPayments"], base=self.info["balancebase"])
             if not result :
@@ -695,9 +697,5 @@
             group.ident = result[0][0]
             group.Name = fields.get("pykotaGroupName", [groupname])[0] 
-            group.LimitBy = fields.get("pykotaLimitBy")
-            if group.LimitBy is not None :
-                group.LimitBy = group.LimitBy[0]
-            else :    
-                group.LimitBy = "quota"
+            group.LimitBy = fields.get("pykotaLimitBy", ["quota"])[0]
             group.AccountBalance = 0.0
             group.LifeTimePaid = 0.0
@@ -717,6 +715,6 @@
             printer.ident = result[0][0]
             printer.Name = fields.get("pykotaPrinterName", [printername])[0] 
-            printer.PricePerJob = float(fields.get("pykotaPricePerJob", [0.0])[0] or 0.0)
-            printer.PricePerPage = float(fields.get("pykotaPricePerPage", [0.0])[0] or 0.0)
+            printer.PricePerJob = float(fields.get("pykotaPricePerJob", [0.0])[0])
+            printer.PricePerPage = float(fields.get("pykotaPricePerPage", [0.0])[0])
             printer.uniqueMember = fields.get("uniqueMember", [])
             printer.Description = self.databaseToUserCharset(fields.get("description", [""])[0]) 
@@ -732,10 +730,11 @@
             else :    
                 base = self.info["userquotabase"]
-            result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaUserName=%s)(pykotaPrinterName=%s))" % (user.Name, printer.Name), ["pykotaPageCounter", "pykotaLifePageCounter", "pykotaSoftLimit", "pykotaHardLimit", "pykotaDateLimit"], base=base)
+            result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaUserName=%s)(pykotaPrinterName=%s))" % (user.Name, printer.Name), ["pykotaPageCounter", "pykotaLifePageCounter", "pykotaSoftLimit", "pykotaHardLimit", "pykotaDateLimit", "pykotaWarnCount"], base=base)
             if result :
                 fields = result[0][1]
                 userpquota.ident = result[0][0]
-                userpquota.PageCounter = int(fields.get("pykotaPageCounter", [0])[0] or 0)
-                userpquota.LifePageCounter = int(fields.get("pykotaLifePageCounter", [0])[0] or 0)
+                userpquota.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
+                userpquota.LifePageCounter = int(fields.get("pykotaLifePageCounter", [0])[0])
+                userpquota.WarnCount = int(fields.get("pykotaWarnCount", [0])[0])
                 userpquota.SoftLimit = fields.get("pykotaSoftLimit")
                 if userpquota.SoftLimit is not None :
@@ -815,5 +814,5 @@
             result = None
             try :
-                result = self.doSearch("objectClass=pykotaJob", ["pykotaJobSizeBytes", "pykotaHostName", "pykotaUserName", "pykotaJobId", "pykotaPrinterPageCounter", "pykotaJobSize", "pykotaAction", "pykotaJobPrice", "pykotaFileName", "pykotaTitle", "pykotaCopies", "pykotaOptions", "createTimestamp"], base="cn=%s,%s" % (lastjobident, self.info["jobbase"]), scope=ldap.SCOPE_BASE)
+                result = self.doSearch("objectClass=pykotaJob", ["pykotaJobSizeBytes", "pykotaHostName", "pykotaUserName", "pykotaJobId", "pykotaPrinterPageCounter", "pykotaJobSize", "pykotaAction", "pykotaJobPrice", "pykotaFileName", "pykotaTitle", "pykotaCopies", "pykotaOptions", "pykotaBillingCode", "pykotaPages", "pykotaMD5Sum", "createTimestamp"], base="cn=%s,%s" % (lastjobident, self.info["jobbase"]), scope=ldap.SCOPE_BASE)
             except PyKotaStorageError :    
                 pass # Last job entry exists, but job probably doesn't exist anymore. 
@@ -823,5 +822,5 @@
                 lastjob.JobId = fields.get("pykotaJobId")[0]
                 lastjob.UserName = fields.get("pykotaUserName")[0]
-                lastjob.PrinterPageCounter = int(fields.get("pykotaPrinterPageCounter", [0])[0] or 0)
+                lastjob.PrinterPageCounter = int(fields.get("pykotaPrinterPageCounter", [0])[0])
                 try :
                     lastjob.JobSize = int(fields.get("pykotaJobSize", [0])[0])
@@ -839,4 +838,7 @@
                 lastjob.JobHostName = fields.get("pykotaHostName", [""])[0]
                 lastjob.JobSizeBytes = fields.get("pykotaJobSizeBytes", [0L])[0]
+                lastjob.JobBillingCode = fields.get("pykotaMD5Sum", [None])[0]
+                lastjob.JobMD5Sum = fields.get("pykotaMD5Sum", [None])[0]
+                lastjob.JobPages = fields.get("pykotaPages", [""])[0]
                 date = fields.get("createTimestamp", ["19700101000000"])[0]
                 year = int(date[:4])
@@ -924,5 +926,5 @@
         else :
            base = self.info["userquotabase"]
-        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s)(|%s))" % (printer.Name, "".join(["(pykotaUserName=%s)" % uname for uname in names])), ["pykotaUserName", "pykotaPageCounter", "pykotaLifePageCounter", "pykotaSoftLimit", "pykotaHardLimit", "pykotaDateLimit"], base=base)
+        result = self.doSearch("(&(objectClass=pykotaUserPQuota)(pykotaPrinterName=%s)(|%s))" % (printer.Name, "".join(["(pykotaUserName=%s)" % uname for uname in names])), ["pykotaUserName", "pykotaPageCounter", "pykotaLifePageCounter", "pykotaSoftLimit", "pykotaHardLimit", "pykotaDateLimit", "pykotaWarnCount"], base=base)
         if result :
             for (userquotaid, fields) in result :
@@ -930,6 +932,7 @@
                 userpquota = StorageUserPQuota(self, user, printer)
                 userpquota.ident = userquotaid
-                userpquota.PageCounter = int(fields.get("pykotaPageCounter", [0])[0] or 0)
-                userpquota.LifePageCounter = int(fields.get("pykotaLifePageCounter", [0])[0] or 0)
+                userpquota.PageCounter = int(fields.get("pykotaPageCounter", [0])[0])
+                userpquota.LifePageCounter = int(fields.get("pykotaLifePageCounter", [0])[0])
+                userpquota.WarnCount = int(fields.get("pykotaWarnCount", [0])[0])
                 userpquota.SoftLimit = fields.get("pykotaSoftLimit")
                 if userpquota.SoftLimit is not None :
@@ -990,4 +993,5 @@
                        "pykotaUserName" : user.Name,
                        "pykotaLimitBy" : (user.LimitBy or "quota"),
+                       "pykotaOverCharge" : str(user.OverCharge),
                     }   
                        
@@ -1048,5 +1052,5 @@
         newfields = { 
                       "pykotaGroupName" : group.Name,
-                      "pykotaLimitBY" : (group.LimitBy or "quota"),
+                      "pykotaLimitBy" : (group.LimitBy or "quota"),
                     } 
         mustadd = 1
@@ -1102,4 +1106,5 @@
                    "pykotaPageCounter" : "0",
                    "pykotaLifePageCounter" : "0",
+                   "pykotaWarnCount" : "0",
                  } 
         if self.info["userquotabase"].lower() == "user" :
@@ -1141,4 +1146,11 @@
         self.doModify(printer.ident, fields)
         
+    def writeUserOverCharge(self, user, factor) :
+        """Sets the user's overcharging coefficient."""
+        fields = {
+                   "pykotaOverCharge" : str(factor),
+                 }
+        self.doModify(user.ident, fields)
+        
     def writeUserLimitBy(self, user, limitby) :    
         """Sets the user's limiting factor."""
@@ -1183,4 +1195,5 @@
                    "pykotaLifePageCounter" : str(newlifepagecounter),
                    "pykotaDateLimit" : None,
+                   "pykotaWarnCount" : "0",
                  }  
         return self.doModify(userpquota.ident, fields)         
@@ -1246,4 +1259,5 @@
                    "pykotaHostName" : str(clienthost), 
                    "pykotaJobSizeBytes" : str(jobsizebytes),
+                   # TODO : add the 3 missing fields
                  }
         if (not self.disablehistory) or (not printer.LastJob.Exists) :
@@ -1278,6 +1292,21 @@
                    "pykotaHardLimit" : str(hardlimit),
                    "pykotaDateLimit" : "None",
+                   "pykotaWarnCount" : "0",
                  }
         self.doModify(userpquota.ident, fields)
+        
+    def writeUserPQuotaWarnCount(self, userpquota, warncount) :
+        """Sets the warn counter value for a user quota."""
+        fields = { 
+                   "pykotaWarnCount" : str(warncount or 0),
+                 }
+        self.doModify(userpquota.ident, fields)
+        
+    def increaseUserPQuotaWarnCount(self, userpquota) :
+        """Increases the warn counter value for a user quota."""
+        fields = {
+                   "pykotaWarnCount" : { "operator" : "+", "value" : 1, "convert" : int },
+                 }
+        return self.doModify(userpquota.ident, fields)         
         
     def writeGroupPQuotaLimits(self, grouppquota, softlimit, hardlimit) :
Index: pykota/trunk/pykota/reporter.py
===================================================================
--- pykota/trunk/pykota/reporter.py (revision 1692)
+++ pykota/trunk/pykota/reporter.py (revision 2054)
@@ -22,4 +22,11 @@
 #
 # $Log$
+# Revision 1.11  2005/02/13 22:02:29  jalet
+# Big database structure changes. Upgrade script is now included as well as
+# the new LDAP schema.
+# Introduction of the -o | --overcharge command line option to edpykota.
+# The output of repykota is more complete, but doesn't fit in 80 columns anymore.
+# Introduction of the new 'maxdenybanners' directive.
+#
 # Revision 1.10  2004/09/02 10:09:30  jalet
 # Fixed bug in LDAP user deletion code which didn't correctly delete the user's
@@ -87,7 +94,7 @@
     def getReportHeader(self) :        
         if self.isgroup :
-            return _("Group           used    soft    hard    balance grace         total       paid")
+            return _("Group          overcharge   used    soft    hard    balance grace         total       paid warn")
         else :    
-            return _("User            used    soft    hard    balance grace         total       paid")
+            return _("User           overcharge   used    soft    hard    balance grace         total       paid warn")
             
     def getPrinterRealPageCounter(self, printer) :        
@@ -109,4 +116,12 @@
         balance = float(entry.AccountBalance or 0.0)
         lifetimepaid = float(entry.LifeTimePaid or 0.0)
+        if not hasattr(entry, "OverCharge") :
+            overcharge = _("N/A")       # Not available for groups
+        else :    
+            overcharge = float(entry.OverCharge or 0.0)
+        if not hasattr(quota, "WarnCount") :    
+            warncount = _("N/A")        # Not available for groups
+        else :    
+            warncount = int(quota.WarnCount or 0)
         
         #balance
@@ -199,5 +214,10 @@
         strbalance = ("%5.2f" % balance)[:10]
         strlifetimepaid = ("%6.2f" % lifetimepaid)[:10]
-        return (lifepagecounter, lifetimepaid, entry.Name, reached, pagecounter, str(quota.SoftLimit), str(quota.HardLimit), strbalance, str(datelimit)[:10], lifepagecounter, strlifetimepaid)
+        strovercharge = ("%5s" % overcharge)[:5]
+        strwarncount = ("%4s" % warncount)[:4]
+        return (lifepagecounter, lifetimepaid, entry.Name, reached, \
+                pagecounter, str(quota.SoftLimit), str(quota.HardLimit), \
+                strbalance, str(datelimit)[:10], lifepagecounter, \
+                strlifetimepaid, strovercharge, strwarncount)
         
 def openReporter(tool, reporttype, printers, ugnames, isgroup) :
Index: pykota/trunk/pykota/storage.py
===================================================================
--- pykota/trunk/pykota/storage.py (revision 2030)
+++ pykota/trunk/pykota/storage.py (revision 2054)
@@ -22,4 +22,11 @@
 #
 # $Log$
+# Revision 1.68  2005/02/13 22:02:29  jalet
+# Big database structure changes. Upgrade script is now included as well as
+# the new LDAP schema.
+# Introduction of the -o | --overcharge command line option to edpykota.
+# The output of repykota is more complete, but doesn't fit in 80 columns anymore.
+# Introduction of the new 'maxdenybanners' directive.
+#
 # Revision 1.67  2005/01/18 19:47:50  jalet
 # Big bug fix wrt the datelimit attribute
@@ -276,4 +283,5 @@
         self.LifeTimePaid = None
         self.Email = None
+        self.OverCharge = 1.0
         self.Payments = [] # TODO : maybe handle this smartly for SQL, for now just don't retrieve them
         
@@ -308,4 +316,9 @@
             self.LimitBy = limitby
         
+    def setOverChargeFactor(self, factor) :    
+        """Sets the user's overcharging coefficient."""
+        self.parent.writeUserOverCharge(self, factor)
+        self.OverCharge = factor
+        
     def delete(self) :    
         """Deletes an user from the Quota Storage."""
@@ -357,4 +370,5 @@
         self.PricePerJob = None
         self.Description = None
+        self.Coefficients = None
         
     def __getattr__(self, name) :    
@@ -424,4 +438,5 @@
         self.HardLimit = None
         self.DateLimit = None
+        self.WarnCount = None
         
     def __getattr__(self, name) :    
@@ -445,4 +460,5 @@
         self.HardLimit = hardlimit
         self.DateLimit = None
+        self.WarnCount = 0
         
     def setUsage(self, used) :
@@ -454,4 +470,5 @@
                 self.parent.increaseUserPQuotaPagesCounters(self, vused)
                 self.parent.writeUserPQuotaDateLimit(self, None)
+                self.parent.writeUserPQuotaWarnCount(self, 0)
             except PyKotaStorageError, msg :    
                 self.parent.rollbackTransaction()
@@ -465,5 +482,11 @@
             self.PageCounter = self.LifePageCounter = vused
         self.DateLimit = None
+        self.WarnCount = 0
 
+    def warn(self) :
+        """Increases the warn counter for this user quota."""
+        self.parent.increaseUserPQuotaWarnCount(self)
+        self.WarnCount = (self.WarnCount or 0) + 1
+        
     def reset(self) :    
         """Resets page counter to 0."""
@@ -482,8 +505,14 @@
         totalprice = 0.0    
         if jobsize :
-            for upq in [ self ] + self.ParentPrintersUserPQuota :
-                price = (float(upq.Printer.PricePerPage or 0.0) * jobsize) + float(upq.Printer.PricePerJob or 0.0)
-                totalprice += price
-        return totalprice    
+            if self.User.OverCharge != 0.0 :    # optimization, but TODO : beware of rounding errors
+                for upq in [ self ] + self.ParentPrintersUserPQuota :
+                    price = (float(upq.Printer.PricePerPage or 0.0) * jobsize) + float(upq.Printer.PricePerJob or 0.0)
+                    totalprice += price
+        if self.User.OverCharge != 1.0 : # TODO : beware of rounding errors
+            overcharged = totalprice * self.User.OverCharge        
+            self.parent.tool.printInfo("Overcharging %s by a factor of %s ===> User %s will be charged for %s units." % (totalprice, self.User.OverCharge, self.User.Name, overcharged))
+            return overcharged
+        else :    
+            return totalprice
             
     def increasePagesUsage(self, jobsize) :
@@ -589,4 +618,7 @@
         self.JobOptions = None
         self.JobHostName = None
+        self.JobMD5Sum = None
+        self.JobPages = None
+        self.JobBillingCode = None
         
     def __getattr__(self, name) :    
Index: pykota/trunk/pykota/config.py
===================================================================
--- pykota/trunk/pykota/config.py (revision 1968)
+++ pykota/trunk/pykota/config.py (revision 2054)
@@ -22,4 +22,11 @@
 #
 # $Log$
+# Revision 1.59  2005/02/13 22:02:29  jalet
+# Big database structure changes. Upgrade script is now included as well as
+# the new LDAP schema.
+# Introduction of the -o | --overcharge command line option to edpykota.
+# The output of repykota is more complete, but doesn't fit in 80 columns anymore.
+# Introduction of the new 'maxdenybanners' directive.
+#
 # Revision 1.58  2004/12/02 22:01:58  jalet
 # TLS is now supported with the LDAP backend
@@ -507,4 +514,19 @@
             return (mailto, args)
         
+    def getMaxDenyBanners(self, printername) :    
+        """Returns the maximum number of deny banners to be printed for a particular user on a particular printer."""
+        try :
+            maxdb = self.getPrinterOption(printername, "maxdenybanners")
+        except PyKotaConfigError :    
+            return 0 # default value is to forbid printing a deny banner.
+        try :
+            value = int(maxdb.strip())
+            if value < 0 :
+                raise ValueError
+        except (TypeError, ValueError) :    
+            raise PyKotaConfigError, _("Invalid maximal deny banners counter %s") % maxdb
+        else :    
+            return value
+            
     def getGraceDelay(self, printername) :    
         """Returns the grace delay in days."""
Index: pykota/trunk/pykota/tool.py
===================================================================
--- pykota/trunk/pykota/tool.py (revision 2008)
+++ pykota/trunk/pykota/tool.py (revision 2054)
@@ -22,4 +22,11 @@
 #
 # $Log$
+# Revision 1.149  2005/02/13 22:02:29  jalet
+# Big database structure changes. Upgrade script is now included as well as
+# the new LDAP schema.
+# Introduction of the -o | --overcharge command line option to edpykota.
+# The output of repykota is more complete, but doesn't fit in 80 columns anymore.
+# Introduction of the new 'maxdenybanners' directive.
+#
 # Revision 1.148  2005/01/06 23:24:21  jalet
 # Regain priviledge the time to open the job's data file when printing in
@@ -1012,16 +1019,20 @@
                 return action        
             else :    
-                val = float(user.AccountBalance or 0.0)
-                enforcement = self.config.getPrinterEnforcement(printer.Name)
-                if enforcement == "STRICT" : 
-                    val -= self.softwareJobPrice # use precomputed size.
-                if val <= 0.0 :
-                    action = "DENY"
-                elif val <= self.config.getPoorMan() :    
-                    action = "WARN"
+                if user.OverCharge == 0.0 :
+                    self.printInfo(_("User %s will not be charged for printing.") % user.Name)
+                    action = "ALLOW"
                 else :
-                    action = "ALLOW"
-                if (enforcement == "STRICT") and (val == 0.0) :
-                    action = "WARN" # we can still print until account is 0
+                    val = float(user.AccountBalance or 0.0)
+                    enforcement = self.config.getPrinterEnforcement(printer.Name)
+                    if enforcement == "STRICT" : 
+                        val -= self.softwareJobPrice # use precomputed size.
+                    if val <= 0.0 :
+                        action = "DENY"
+                    elif val <= self.config.getPoorMan() :    
+                        action = "WARN"
+                    else :
+                        action = "ALLOW"
+                    if (enforcement == "STRICT") and (val == 0.0) :
+                        action = "WARN" # we can still print until account is 0
                 return action    
         else :
@@ -1106,4 +1117,5 @@
         if action.startswith("POLICY_") :
             action = action[7:]
+            
         if action == "DENY" :
             adminmessage = _("Print Quota exceeded for user %s on printer %s") % (user.Name, printer.Name)
@@ -1286,4 +1298,5 @@
     def exportUserInfo(self, userpquota) :
         """Exports user information to the environment."""
+        os.environ["PYKOTAOVERCHARGE"] = str(userpquota.User.OverCharge)
         os.environ["PYKOTALIMITBY"] = str(userpquota.User.LimitBy)
         os.environ["PYKOTABALANCE"] = str(userpquota.User.AccountBalance or 0.0)
@@ -1294,4 +1307,5 @@
         os.environ["PYKOTAHARDLIMIT"] = str(userpquota.HardLimit)
         os.environ["PYKOTADATELIMIT"] = str(userpquota.DateLimit)
+        os.environ["PYKOTAWARNCOUNT"] = str(userpquota.WarnCount)
         
         # not really an user information, but anyway
Index: pykota/trunk/pykota/version.py
===================================================================
--- pykota/trunk/pykota/version.py (revision 2044)
+++ pykota/trunk/pykota/version.py (revision 2054)
@@ -22,5 +22,5 @@
 #
 
-__version__ = "1.21alpha23_unofficial"
+__version__ = "1.21alpha24_unofficial"
 
 __doc__ = """PyKota : a complete Printing Quota Solution for CUPS and LPRng."""
Index: pykota/trunk/FAQ
===================================================================
--- pykota/trunk/FAQ (revision 2047)
+++ pykota/trunk/FAQ (revision 2054)
@@ -19,4 +19,7 @@
     that all debug messages for PyKota will go to syslog's
     destination for the LPR syslog facility.
+    
+    IMPORTANT : please don't send log files to the mailing list
+    unless they are very small of unless someone asked for this.
   
   * How can I limit my users to N pages per day (month/year) ?
@@ -47,4 +50,10 @@
       use the --printer command line argument as well. See
       edpykota's documentation for details.
+      
+    Another way to do this when you limit your users by balance, is  
+    to set an overcharging factor of zero for every user you want to
+    print with no limitation :
+    
+      $ edpykota --add --limitby balance --overcharge 0 user1 user2 ...
   
   * Nothing prints when my printer is in PowerSave mode, what
Index: pykota/trunk/initscripts/ldap/pykota.schema
===================================================================
--- pykota/trunk/initscripts/ldap/pykota.schema (revision 2028)
+++ pykota/trunk/initscripts/ldap/pykota.schema (revision 2054)
@@ -187,7 +187,7 @@
         SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
         
-# pykotaUserCoefficient
-attributetype ( 1.3.6.1.4.1.16868.1.1.27 NAME 'pykotaUserCoefficient'
-        DESC 'Coefficient for a particular user, float'
+# pykotaOverCharge
+attributetype ( 1.3.6.1.4.1.16868.1.1.27 NAME 'pykotaOverCharge'
+        DESC 'OverCharging factor for a particular user, float'
         EQUALITY caseIgnoreIA5Match
         SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
@@ -270,5 +270,5 @@
 objectclass ( 1.3.6.1.4.1.16868.1.2.7 NAME 'pykotaAccountBalance' SUP top AUXILIARY
         DESC 'PyKota User account balance'
-        MAY  ( pykotaUserName $ pykotaBalance $ pykotaLifeTimePaid $ pykotaUserCoefficient $ pykotaPayments ) )
+        MAY  ( pykotaUserName $ pykotaBalance $ pykotaLifeTimePaid $ pykotaOverCharge $ pykotaPayments ) )
         
 # pykotaLastJob        
Index: pykota/trunk/initscripts/postgresql/README.postgresql
===================================================================
--- pykota/trunk/initscripts/postgresql/README.postgresql (revision 2028)
+++ pykota/trunk/initscripts/postgresql/README.postgresql (revision 2054)
@@ -59,4 +59,15 @@
   version of PyKota.
   
+  * An SQL script to upgrade a 1.20 PyKota Storage DataBase to
+    1.21 is included. Launch it this way on the Quota Storage Server :
+    
+        $ psql -U postgres pykota
+        pykota=# \i upgrade-to-1.21.sql
+        pykota=# \q
+        $
+        
+    This script adds many fields to existing tables, and also adds
+    some tables in indexes.
+    
   * An SQL script to upgrade a 1.18 PyKota Storage DataBase to
     1.19 is included. Launch it this way on the Quota Storage Server :
Index: pykota/trunk/initscripts/postgresql/pykota-postgresql.sql
===================================================================
--- pykota/trunk/initscripts/postgresql/pykota-postgresql.sql (revision 2037)
+++ pykota/trunk/initscripts/postgresql/pykota-postgresql.sql (revision 2054)
@@ -20,4 +20,11 @@
 --
 -- $Log$
+-- Revision 1.16  2005/02/13 22:02:29  jalet
+-- Big database structure changes. Upgrade script is now included as well as
+-- the new LDAP schema.
+-- Introduction of the -o | --overcharge command line option to edpykota.
+-- The output of repykota is more complete, but doesn't fit in 80 columns anymore.
+-- Introduction of the new 'maxdenybanners' directive.
+--
 -- Revision 1.15  2005/01/23 10:58:22  jalet
 -- Added a few indexes for the database
@@ -106,5 +113,5 @@
                    lifetimepaid FLOAT DEFAULT 0.0,
                    limitby TEXT DEFAULT 'quota',
-                   coefficient FLOAT NOT NULL DEFAULT 1.0);
+                   overcharge FLOAT NOT NULL DEFAULT 1.0);
                    
 --
@@ -135,5 +142,5 @@
                         hardlimit INT4,
                         datelimit TIMESTAMP,
-                        warned INT4 DEFAULT 0); -- not a boolean, will help stats
+                        warncount INT4 DEFAULT 0); 
 CREATE INDEX userpquota_u_id_ix ON userpquota (userid);
 CREATE INDEX userpquota_p_id_ix ON userpquota (printerid);
@@ -176,4 +183,6 @@
                          hardlimit INT4,
                          datelimit TIMESTAMP);
+CREATE INDEX grouppquota_g_id_ix ON grouppquota (groupid);
+CREATE INDEX grouppquota_p_id_ix ON grouppquota (printerid);
 CREATE UNIQUE INDEX grouppquota_up_id_ix ON grouppquota (groupid, printerid);
                         
@@ -206,5 +215,5 @@
                            printerid INTEGER NOT NULL REFERENCES printers(id), 
                            label TEXT NOT NULL, 
-                           coefficient FLOAT NOT NULL DEFAULT 1.0, 
+                           coefficient FLOAT DEFAULT 1.0, 
                            CONSTRAINT coeffconstraint UNIQUE (printerid, label));
 
@@ -212,6 +221,6 @@
 -- Set some ACLs                        
 --
-REVOKE ALL ON users, groups, printers, userpquota, grouppquota, groupsmembers, printergroupsmembers, jobhistory, payments FROM public;                        
-REVOKE ALL ON users_id_seq, groups_id_seq, printers_id_seq, userpquota_id_seq, grouppquota_id_seq, jobhistory_id_seq, payments_id_seq FROM public;
+REVOKE ALL ON users, groups, printers, userpquota, grouppquota, groupsmembers, printergroupsmembers, jobhistory, payments, coefficients FROM public;                        
+REVOKE ALL ON users_id_seq, groups_id_seq, printers_id_seq, userpquota_id_seq, grouppquota_id_seq, jobhistory_id_seq, payments_id_seq, coefficients_id_seq FROM public;
 
 GRANT SELECT, INSERT, UPDATE, DELETE, REFERENCES ON users, groups, printers, userpquota, grouppquota, groupsmembers, printergroupsmembers, jobhistory, payments, coefficients TO pykotaadmin;
