Ticket #11: pykota-costing-rework.patch

File pykota-costing-rework.patch, 23.0 kB (added by jerome, 13 years ago)

Patch to improve the computation of a print job's cost.

  • pykota/accounters/ink.py

    diff --git a/pykota/accounters/ink.py b/pykota/accounters/ink.py
    index 42889f5..4e795d1 100644
    a b  
    3232                        "CMYK" : { "C" : "cyan", "M" : "magenta", "Y" : "yellow", "K" : "black" } , 
    3333                        "CMY" : { "C" : "cyan", "M" : "magenta", "Y" : "yellow" } , 
    3434                        "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" } , 
    3737                     } 
    3838    def computeJobSize(self) : 
    3939        """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 
     27import os 
     28import imp 
     29 
     30from pykota.errors import PyKotaAccounterError 
     31from pykota.accounter import AccounterBase 
     32 
     33class 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  
    4747            return self.withInternalParser() 
    4848 
    4949    def withInternalParser(self) : 
    50         """Does software accounting through an external script.""" 
     50        """Does software accounting through an internal script.""" 
    5151        jobsize = 0 
    5252        if self.filter.JobSizeBytes : 
    5353            try : 
  • pykota/config.py

    diff --git a/pykota/config.py b/pykota/config.py
    index 954fc70..29d33c8 100644
    a b  
    33# PyKota : Print Quotas for CUPS 
    44# 
    55# (c) 2003-2009 Jerome Alet <alet@librelogiciel.com> 
     6# Portions (c) 2009 Trever L. Adams <trever.adams@gmail.com> 
     7# 
    68# This program is free software: you can redistribute it and/or modify 
    79# it under the terms of the GNU General Public License as published by 
    810# the Free Software Foundation, either version 3 of the License, or 
     
    192194 
    193195    def getPreAccounterBackend(self, printername) : 
    194196        """Returns the preaccounter backend to use for a given printer.""" 
    195         validaccounters = [ "software", "ink" ] 
     197        validaccounters = [ "software", "ink", "media" ] 
    196198        try : 
    197199            fullaccounter = self.getPrinterOption(printername, "preaccounter").strip() 
    198200        except PyKotaConfigError : 
     
    207209                        raise PyKotaConfigError, _("Invalid preaccounter %s for printer %s") % (fullaccounter, printername) 
    208210                    if args.endswith(')') : 
    209211                        args = args[:-1].strip() 
    210                     if (vac == "ink") and not args : 
     212                    if (vac in ("ink", "media")) and not args : 
    211213                        raise PyKotaConfigError, _("Invalid preaccounter %s for printer %s") % (fullaccounter, printername) 
    212214                    return (vac, args) 
    213215            raise PyKotaConfigError, _("Option preaccounter in section %s only supports values in %s") % (printername, str(validaccounters)) 
    214216 
    215217    def getAccounterBackend(self, printername) : 
    216218        """Returns the accounter backend to use for a given printer.""" 
    217         validaccounters = [ "hardware", "software", "ink" ] 
     219        validaccounters = [ "hardware", "software", "ink", "media" ] 
    218220        try : 
    219221            fullaccounter = self.getPrinterOption(printername, "accounter").strip() 
    220222        except PyKotaConfigError : 
     
    229231                        raise PyKotaConfigError, _("Invalid accounter %s for printer %s") % (fullaccounter, printername) 
    230232                    if args.endswith(')') : 
    231233                        args = args[:-1].strip() 
    232                     if (vac in ("hardware", "ink")) and not args : 
     234                    if (vac in ("hardware", "ink", "media")) and not args : 
    233235                        raise PyKotaConfigError, _("Invalid accounter %s for printer %s") % (fullaccounter, printername) 
    234236                    return (vac, args) 
    235237            raise PyKotaConfigError, _("Option accounter in section %s only supports values in %s") % (printername, str(validaccounters)) 
     
    724726                raise PyKotaConfigError, _("Option trustjobsize for printer %s is incorrect") % printername 
    725727            return (limit, replacement) 
    726728 
    727     def getPrinterCoefficients(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_" 
    730732        try : 
    731733            globalbranches = [ (k, self.config.get("global", k).decode(self.config_charset)) for k in self.config.options("global") if k.startswith(branchbasename) ] 
    732734        except ConfigParser.NoSectionError, msg : 
     
    743745                try : 
    744746                    branches[k] = float(value) 
    745747                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) 
    747749 
    748750        for (k, v) in sectionbranches : 
    749751            k = k.split('_', 1)[1] 
     
    752754                try : 
    753755                    branches[k] = float(value) # overwrite any global option or set a new value 
    754756                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) 
    756758            else : 
    757759                del branches[k] # empty value disables a global option 
    758760        return branches 
  • pykota/storage.py

    diff --git a/pykota/storage.py b/pykota/storage.py
    index f84e0ef..d0ee800 100644
    a b  
    33# PyKota : Print Quotas for CUPS 
    44# 
    55# (c) 2003-2009 Jerome Alet <alet@librelogiciel.com> 
     6# (c) 2009 Trever L. Adams <trever.adams@gmail.com> 
     7# 
    68# This program is free software: you can redistribute it and/or modify 
    79# it under the terms of the GNU General Public License as published by 
    810# the Free Software Foundation, either version 3 of the License, or 
     
    2830 
    2931from pykota.errors import PyKotaStorageError 
    3032 
     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 
     39MediaSizeArea = { "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 
    3196class StorageObject : 
    3297    """Object present in the database.""" 
    3398    def __init__(self, parent) : 
     
    178243            self.LastJob = self.parent.getPrinterLastJob(self) 
    179244            self.parent.tool.logdebug("Lazy retrieval of last job for printer %s" % self.Name) 
    180245            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) 
    185274        else : 
    186275            raise AttributeError, name 
    187276 
     
    237326        self.Exists = False 
    238327        self.isDirty = False 
    239328 
     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 
    240341 
    241342class StorageUserPQuota(StorageObject) : 
    242343    """User Print Quota class.""" 
     
    312413    def computeJobPrice(self, jobsize, inkusage=[]) : 
    313414        """Computes the job price as the sum of all parent printers' prices + current printer's ones.""" 
    314415        totalprice = 0.0 
     416        chargemedia = 1 
     417        pagevariance = 1.0 
    315418        if jobsize : 
    316419            if self.User.OverCharge != 0.0 :    # optimization, but TODO : beware of rounding errors 
    317420                for upq in [ self ] + self.ParentPrintersUserPQuota : 
     
    320423                    if not inkusage : 
    321424                        totalprice += (jobsize * pageprice) 
    322425                    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] 
    323431                        for pageindex in range(jobsize) : 
    324432                            try : 
    325433                                usage = inkusage[pageindex] 
     
    327435                                self.parent.tool.logdebug("No ink usage information. Using base cost of %f credits for page %i." % (pageprice, pageindex+1)) 
    328436                                totalprice += pageprice 
    329437                            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))                                     
    331470                                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 
    337483        if self.User.OverCharge != 1.0 : # TODO : beware of rounding errors 
    338484            overcharged = totalprice * self.User.OverCharge 
    339485            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))