Ticket #11: pykota-costing-rework.patch
File pykota-costing-rework.patch, 23.0 kB (added by jerome, 15 years ago) |
---|
-
pykota/accounters/ink.py
diff --git a/pykota/accounters/ink.py b/pykota/accounters/ink.py index 42889f5..4e795d1 100644
a b 32 32 "CMYK" : { "C" : "cyan", "M" : "magenta", "Y" : "yellow", "K" : "black" } , 33 33 "CMY" : { "C" : "cyan", "M" : "magenta", "Y" : "yellow" } , 34 34 "RGB" : { "R" : "red", "G" : "green", "B" : "blue" } , 35 "BW" : { " B" : "black", "W" : "white" } ,36 "GC" : { "G " : "grayscale", "C" : "colored" } ,35 "BW" : { "K" : "black", "W" : "white" } , 36 "GC" : { "GS" : "grayscale", "CR" : "colored" } , 37 37 } 38 38 def computeJobSize(self) : 39 39 """Do ink accounting for a print job.""" -
(a) /dev/null vs. (b) b/pykota/accounters/media.py
diff --git a/pykota/accounters/media.py b/pykota/accounters/media.py new file mode 100644 index 0000000..578c50b
a b 1 # -*- coding: utf-8 -*- 2 # 3 # PyKota : Print Quotas for CUPS 4 # 5 # (c) 2009 Trever L. Adams <trever.adams@gmail.com> 6 # (c) 2003-2009 Jerome Alet <alet@librelogiciel.com> 7 # 8 # This program is free software: you can redistribute it and/or modify 9 # it under the terms of the GNU General Public License as published by 10 # the Free Software Foundation, either version 3 of the License, or 11 # (at your option) any later version. 12 # 13 # This program is distributed in the hope that it will be useful, 14 # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 # GNU General Public License for more details. 17 # 18 # You should have received a copy of the GNU General Public License 19 # along with this program. If not, see <http://www.gnu.org/licenses/>. 20 # 21 # $Id$ 22 # 23 # 24 25 """This module handles ink accounting in PyKota.""" 26 27 import os 28 import imp 29 30 from pykota.errors import PyKotaAccounterError 31 from pykota.accounter import AccounterBase 32 33 class Accounter(AccounterBase) : 34 mspacelabels = { "MT" : "media_type", "MS" : "media_size", "DU" : "media_duplexed" } 35 def computeJobSize(self) : 36 """Do ink accounting for a print job.""" 37 if (not self.isPreAccounter) and \ 38 (self.filter.accounter.arguments == self.filter.preaccounter.arguments) : 39 # if precomputing has been done and both accounter and preaccounter are 40 # configured the same, no need to launch a second pass since we already 41 # know the result. 42 self.filter.logdebug("Precomputing pass told us that job is %s pages long." % self.filter.softwareJobSize) 43 self.inkUsage = self.filter.preaccounter.inkUsage # Optimize : already computed ! 44 return self.filter.softwareJobSize # Optimize : already computed ! 45 46 parameters = [p.strip() for p in self.arguments.split(',')] 47 if len(parameters) == 0 : 48 parameters.append("none") 49 50 if parameters[0] not in ("none", "ink") : 51 raise PyKotaAccounterError, _("Invalid parameters for media accounter : [%s]") % self.arguments 52 53 if parameters[0] == "ink" : 54 if len(parameters) == 2 : 55 parameters.append("72") 56 (myaccounter, colorspace, resolution) = parameters 57 inkbackend = imp.load_source("inkbackend", 58 os.path.join(os.path.dirname(__file__), 59 "%s.py" % myaccounter.lower())) 60 args = colorspace + "," + resolution 61 inkAccounter = inkbackend.Accounter(self.filter, args, 1, myaccounter.lower()) 62 jobsize = inkAccounter.computeJobSize() 63 inkCoverage = list(inkAccounter.inkUsage) 64 self.inkUsage = [] 65 66 jobsize = 0 67 if self.filter.JobSizeBytes : 68 try : 69 from pkpgpdls import analyzer, pdlparser 70 except ImportError : 71 self.filter.printInfo("pkpgcounter is now distributed separately, please grab it from http://www.pykota.com/software/pkpgcounter", "error") 72 self.filter.printInfo("Precomputed job size will be forced to 0 pages.", "error") 73 else : 74 options = analyzer.AnalyzerOptions() 75 try : 76 parser = analyzer.PDLAnalyzer(self.filter.DataFile, options) 77 (mspace, pages) = parser.getMediaFormat() 78 except pdlparser.PDLParserError, msg : 79 # Here we just log the failure, but 80 # we finally ignore it and return 0 since this 81 # computation is just an indication of what the 82 # job's size MAY be. 83 self.filter.printInfo(_("Unable to precompute the job's size, ink coverage, media size and format with the generic PDL analyzer : %s") % msg, "warn") 84 else : 85 localpagecount = 0 86 for page in pages : 87 mediadict = {} 88 for media in page.keys() : 89 mediadict[self.mspacelabels[media]] = page[media] 90 if parameters[0] == "ink" : 91 for color in inkCoverage[localpagecount].keys() : 92 mediadict[color] = inkCoverage[localpagecount][color] 93 self.inkUsage.append(mediadict) 94 localpagecount += 1 95 jobsize = len(pages) 96 try : 97 if self.filter.Ticket.FileName is not None : 98 # when a filename is passed as an argument, the backend 99 # must generate the correct number of copies. 100 jobsize *= self.filter.Ticket.Copies 101 self.inkUsage *= self.filter.Ticket.Copies 102 except AttributeError : # When not run from the cupspykota backend 103 pass 104 self.filter.logdebug("Media usage : %s ===> %s" % (mspace, repr(self.inkUsage))) 105 return jobsize 106 -
pykota/accounters/software.py
diff --git a/pykota/accounters/software.py b/pykota/accounters/software.py index 22e1557..0c5f8cc 100644
a b 47 47 return self.withInternalParser() 48 48 49 49 def withInternalParser(self) : 50 """Does software accounting through an external script."""50 """Does software accounting through an internal script.""" 51 51 jobsize = 0 52 52 if self.filter.JobSizeBytes : 53 53 try : -
pykota/config.py
diff --git a/pykota/config.py b/pykota/config.py index 954fc70..29d33c8 100644
a b 3 3 # PyKota : Print Quotas for CUPS 4 4 # 5 5 # (c) 2003-2009 Jerome Alet <alet@librelogiciel.com> 6 # Portions (c) 2009 Trever L. Adams <trever.adams@gmail.com> 7 # 6 8 # This program is free software: you can redistribute it and/or modify 7 9 # it under the terms of the GNU General Public License as published by 8 10 # the Free Software Foundation, either version 3 of the License, or … … 192 194 193 195 def getPreAccounterBackend(self, printername) : 194 196 """Returns the preaccounter backend to use for a given printer.""" 195 validaccounters = [ "software", "ink" ]197 validaccounters = [ "software", "ink", "media" ] 196 198 try : 197 199 fullaccounter = self.getPrinterOption(printername, "preaccounter").strip() 198 200 except PyKotaConfigError : … … 207 209 raise PyKotaConfigError, _("Invalid preaccounter %s for printer %s") % (fullaccounter, printername) 208 210 if args.endswith(')') : 209 211 args = args[:-1].strip() 210 if (vac == "ink") and not args :212 if (vac in ("ink", "media")) and not args : 211 213 raise PyKotaConfigError, _("Invalid preaccounter %s for printer %s") % (fullaccounter, printername) 212 214 return (vac, args) 213 215 raise PyKotaConfigError, _("Option preaccounter in section %s only supports values in %s") % (printername, str(validaccounters)) 214 216 215 217 def getAccounterBackend(self, printername) : 216 218 """Returns the accounter backend to use for a given printer.""" 217 validaccounters = [ "hardware", "software", "ink" ]219 validaccounters = [ "hardware", "software", "ink", "media" ] 218 220 try : 219 221 fullaccounter = self.getPrinterOption(printername, "accounter").strip() 220 222 except PyKotaConfigError : … … 229 231 raise PyKotaConfigError, _("Invalid accounter %s for printer %s") % (fullaccounter, printername) 230 232 if args.endswith(')') : 231 233 args = args[:-1].strip() 232 if (vac in ("hardware", "ink" )) and not args :234 if (vac in ("hardware", "ink", "media")) and not args : 233 235 raise PyKotaConfigError, _("Invalid accounter %s for printer %s") % (fullaccounter, printername) 234 236 return (vac, args) 235 237 raise PyKotaConfigError, _("Option accounter in section %s only supports values in %s") % (printername, str(validaccounters)) … … 724 726 raise PyKotaConfigError, _("Option trustjobsize for printer %s is incorrect") % printername 725 727 return (limit, replacement) 726 728 727 def getPrinter Coefficients(self, printername) :728 """Returns a mapping of coefficients for a particular printer."""729 branchbasename = " coefficient_"729 def getPrinterInkCosts(self, printername) : 730 """Returns a mapping of inkcosts for a particular printer.""" 731 branchbasename = "inkcost_" 730 732 try : 731 733 globalbranches = [ (k, self.config.get("global", k).decode(self.config_charset)) for k in self.config.options("global") if k.startswith(branchbasename) ] 732 734 except ConfigParser.NoSectionError, msg : … … 743 745 try : 744 746 branches[k] = float(value) 745 747 except ValueError : 746 raise PyKotaConfigError, "Invalid coefficient %s (%s) for printer %s" % (k, value, printername)748 raise PyKotaConfigError, "Invalid inkcost %s (%s) for printer %s" % (k, value, printername) 747 749 748 750 for (k, v) in sectionbranches : 749 751 k = k.split('_', 1)[1] … … 752 754 try : 753 755 branches[k] = float(value) # overwrite any global option or set a new value 754 756 except ValueError : 755 raise PyKotaConfigError, "Invalid coefficient %s (%s) for printer %s" % (k, value, printername)757 raise PyKotaConfigError, "Invalid inkcost %s (%s) for printer %s" % (k, value, printername) 756 758 else : 757 759 del branches[k] # empty value disables a global option 758 760 return branches -
pykota/storage.py
diff --git a/pykota/storage.py b/pykota/storage.py index f84e0ef..d0ee800 100644
a b 3 3 # PyKota : Print Quotas for CUPS 4 4 # 5 5 # (c) 2003-2009 Jerome Alet <alet@librelogiciel.com> 6 # (c) 2009 Trever L. Adams <trever.adams@gmail.com> 7 # 6 8 # This program is free software: you can redistribute it and/or modify 7 9 # it under the terms of the GNU General Public License as published by 8 10 # the Free Software Foundation, either version 3 of the License, or … … 28 30 29 31 from pykota.errors import PyKotaStorageError 30 32 33 # These numbers are area in square millimeters. 34 # Any zeros below indicate that the exact size is unknown. 35 # Below are some papers that have multiple names. Don't use the Alias(es). 36 # Cannocal Alias 37 # Leder 11x17 38 # Statement half letter 39 MediaSizeArea = { "A0" : 999949, 40 "A1" : 499554, 41 "A2" : 249480, 42 "A3" : 124740, 43 "A4" : 62370, 44 "A5" : 31080, 45 "A6" : 15540, 46 "A7" : 7770, 47 "A8" : 3848, 48 "A9" : 1924, 49 "A10" : 962, 50 "ArchA" : 2743.2, 51 "ArchB" : 5486.4, 52 "ArchC" : 10972.8, 53 "ArchD" : 21945.6, 54 "ArchE" : 43891.2, 55 "B0" : 1414000, 56 "B1" : 707000, 57 "B2" : 353500, 58 "B3" : 176500, 59 "B4" : 88250, 60 "B5" : 44000, 61 "B6" : 22000, 62 "C0" : 1189349, 63 "C1" : 594216, 64 "C2" : 296784, 65 "C3" : 148392, 66 "C4" : 74196, 67 "C5" : 37098, 68 "C6" : 18468, 69 "COM10Envelope" : 0, 70 "DL Envelope" : 24200, 71 "Envelope #9" : 873.52, 72 "Envelope #10" : 995.36, 73 "Envelope #11" : 1185.86, 74 "Envelope #12" : 1117.6, 75 "Envelope #14" : 1460.5, 76 "Executive" : 48387, 77 "Foolscap" : 71280, 78 "JDoublePostcard" : 0, 79 "JIS16K" : 0, 80 "JIS8K" : 0, 81 "JISB0" : 0, 82 "JISB1" : 0, 83 "JISB2" : 0, 84 "JISB3" : 0, 85 "JISB4" : 0, 86 "JISB5" : 0, 87 "JISB6" : 0, 88 "JISExec" : 0, 89 "JPostcard" : 0, 90 "Ledger" : 4749.8, 91 "Legal" : 3022.6, 92 "Letter" : 2374.9, 93 "MonarchEnvelope" : 738.19, 94 "Statement" : 1187.45 } 95 31 96 class StorageObject : 32 97 """Object present in the database.""" 33 98 def __init__(self, parent) : … … 178 243 self.LastJob = self.parent.getPrinterLastJob(self) 179 244 self.parent.tool.logdebug("Lazy retrieval of last job for printer %s" % self.Name) 180 245 return self.LastJob 181 elif name == "Coefficients" : 182 self.Coefficients = self.parent.tool.config.getPrinterCoefficients(self.Name) 183 self.parent.tool.logdebug("Lazy retrieval of coefficients for printer %s : %s" % (self.Name, self.Coefficients)) 184 return self.Coefficients 246 elif name == "InkCosts" : 247 InkCosts = self.parent.tool.config.getPrinterInkCosts(self.Name) 248 self.parent.tool.logdebug("Lazy retrieval of inkcosts for printer %s : %s" % (self.Name, InkCosts)) 249 return InkCosts 250 elif name == "DefaultMediaSize" : 251 DefaultMediaSize = self.parent.tool.config.getPrinterOption(self.Name, "default_media_size") 252 if DefaultMediaSize is None : 253 DefaultMediaSize = self.parent.tool.config.getGlobalOption(self.Name, "default_media_size") 254 if DefaultMediaSize is None : 255 DefaultMediaSize = "Letter" 256 self.parent.tool.logdebug("Lazy retrieval of Default Media Size for printer %s : %s" % (self.Name, DefaultMediaSize)) 257 return DefaultMediaSize 258 elif name == "DefaultMediaType" : 259 DefaultMediaType = self.parent.tool.config.getPrinterOption(self.Name, "default_media_type") 260 if DefaultMediaType is None : 261 DefaultMediaType = self.parent.tool.config.getGlobalOption(self.Name, "default_media_type") 262 if DefaultMediaType is None : 263 DefaultMediaType = "Plain" 264 self.parent.tool.logdebug("Lazy retrieval of Default Media Type for printer %s : %s" % (self.Name, DefaultMediaType)) 265 return DefaultMediaType 266 elif name == "ImpressionCost" : 267 ImpressionCost = self.parent.tool.config.getPrinterOption(self.Name, "impression_cost") 268 if ImpressionCost is None : 269 ImpressionCost = self.parent.tool.config.getGlobalOption(self.Name, "impression_cost") 270 if ImpressionCost is None : 271 ImpressionCost = 0.0 272 self.parent.tool.logdebug("Lazy retrieval of Impression Cost for printer %s : %s" % (self.Name, ImpressionCost)) 273 return float(ImpressionCost) 185 274 else : 186 275 raise AttributeError, name 187 276 … … 237 326 self.Exists = False 238 327 self.isDirty = False 239 328 329 def getMediaCost(self, media_type, media_size) : 330 MediaCost = self.parent.tool.config.getPrinterOption(self.Name, "mediacost_"+media_type+"_"+media_size) 331 if MediaCost is None : 332 MediaCost = self.parent.tool.config.getGlobalOption(self.Name, "mediacost_"+media_type+"_"+media_size) 333 if MediaCost is None : 334 MediaCost = self.parent.tool.config.getPrinterOption(self.Name, "mediacost_"+default) 335 if MediaCost is None : 336 MediaCost = self.parent.tool.config.getGlobalOption(self.Name, "mediacost_"+default) 337 if MediaCost is None : 338 MediaCost = 0.00 339 return float(MediaCost) 340 240 341 241 342 class StorageUserPQuota(StorageObject) : 242 343 """User Print Quota class.""" … … 312 413 def computeJobPrice(self, jobsize, inkusage=[]) : 313 414 """Computes the job price as the sum of all parent printers' prices + current printer's ones.""" 314 415 totalprice = 0.0 416 chargemedia = 1 417 pagevariance = 1.0 315 418 if jobsize : 316 419 if self.User.OverCharge != 0.0 : # optimization, but TODO : beware of rounding errors 317 420 for upq in [ self ] + self.ParentPrintersUserPQuota : … … 320 423 if not inkusage : 321 424 totalprice += (jobsize * pageprice) 322 425 else : 426 inkcosts = upq.Printer.InkCosts 427 impressioncost = upq.Printer.ImpressionCost 428 defaultmediatype = upq.Printer.DefaultMediaType 429 defaultmediasize = upq.Printer.DefaultMediaSize 430 default_media_size_area = MediaSizeArea[defaultmediasize] 323 431 for pageindex in range(jobsize) : 324 432 try : 325 433 usage = inkusage[pageindex] … … 327 435 self.parent.tool.logdebug("No ink usage information. Using base cost of %f credits for page %i." % (pageprice, pageindex+1)) 328 436 totalprice += pageprice 329 437 else : 330 coefficients = upq.Printer.Coefficients 438 try : 439 media_type = usage["media_type"] 440 except : 441 media_type = defaultmediatype 442 inkusage[pageindex]["media_type"] = media_type 443 try : 444 media_size = usage["media_size"] 445 except : 446 media_size = defaultmediasize 447 inkusage[pageindex]["media_size"] = media_size 448 mediacost = upq.Printer.getMediaCost(media_type, media_size) 449 450 try : # media_duplex must be set on the first set in the duplex set, not the second 451 if pageindex > 0 : 452 if inkusage[pageindex-1]["media_type"] != media_type or inkusage[pageindex-1]["media_size"] != media_size : 453 chargemedia = 1 # If media size or type changes, duplexing is not active 454 if chargemedia == 1 : 455 totalprice += mediacost 456 self.parent.tool.logdebug("Applying media cost %f for media type %s and media size %s (on page %i) on printer %s" % (mediacost, usage["media_type"], usage["media_size"], pageindex+1, upq.Printer.Name)) 457 458 try : 459 if usage["media_duplexed"] == "Y" : # If we are duplexing with the following page, the next page shouldn't have media costs 460 chargemedia = 0 461 except : 462 pass 463 else : 464 chargemedia = 1 465 except : 466 pass 467 468 totalprice += impressioncost 469 self.parent.tool.logdebug("Applying impressioncost %f (on page %i) on printer %s" % (impressioncost, pageindex+1, upq.Printer.Name)) 331 470 for (ink, value) in usage.items() : 332 coefvalue = coefficients.get(ink, 1.0) 333 coefprice = (coefvalue * pageprice) / 100.0 334 inkprice = coefprice * value 335 self.parent.tool.logdebug("Applying coefficient %f for color %s (used at %f%% on page %i) to base cost %f on printer %s gives %f" % (coefvalue, ink, value, pageindex+1, pageprice, upq.Printer.Name, inkprice)) 336 totalprice += inkprice 471 if ink not in ("media_type", "media_size", "media_duplexed") : # We must ignore non-ink inks 472 inkvalue = inkcosts.get(ink, 1.0) 473 if ink in ["grayscale", "colored"] : 474 inkprice = (inkvalue * value) / 100.0 475 else : 476 try : 477 pagevariance = MediaSizeArea[media_size] / default_media_size_area 478 except : 479 pagevariance = 1.0 480 inkprice = (inkvalue * value) / 100.0 * pagevariance 481 self.parent.tool.logdebug("Applying inkcost %f, with page size variance %f, for color %s (used at %f%% on page %i) on printer %s gives %f" % (inkvalue, pagevariance, ink, value, pageindex+1, upq.Printer.Name, inkprice)) 482 totalprice += inkprice 337 483 if self.User.OverCharge != 1.0 : # TODO : beware of rounding errors 338 484 overcharged = totalprice * self.User.OverCharge 339 485 self.parent.tool.logdebug("Overcharging %s by a factor of %s ===> User %s will be charged for %s credits." % (totalprice, self.User.OverCharge, self.User.Name, overcharged))