1 | #! /usr/bin/env python |
---|
2 | # -*- coding: utf-8 -*- |
---|
3 | # |
---|
4 | # PyKota : Print Quotas for CUPS |
---|
5 | # |
---|
6 | # (c) 2003-2013 Jerome Alet <alet@librelogiciel.com> |
---|
7 | # This program is free software: you can redistribute it and/or modify |
---|
8 | # it under the terms of the GNU General Public License as published by |
---|
9 | # the Free Software Foundation, either version 3 of the License, or |
---|
10 | # (at your option) any later version. |
---|
11 | # |
---|
12 | # This program is distributed in the hope that it will be useful, |
---|
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
---|
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
---|
15 | # GNU General Public License for more details. |
---|
16 | # |
---|
17 | # You should have received a copy of the GNU General Public License |
---|
18 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
---|
19 | # |
---|
20 | # $Id$ |
---|
21 | # |
---|
22 | # |
---|
23 | |
---|
24 | """A refunding tool for PyKota""" |
---|
25 | |
---|
26 | import sys |
---|
27 | import os |
---|
28 | import pwd |
---|
29 | import time |
---|
30 | import cStringIO |
---|
31 | |
---|
32 | from mx import DateTime |
---|
33 | |
---|
34 | try : |
---|
35 | from reportlab.pdfgen import canvas |
---|
36 | from reportlab.lib import pagesizes |
---|
37 | from reportlab.lib.units import cm |
---|
38 | except ImportError : |
---|
39 | hasRL = False |
---|
40 | else : |
---|
41 | hasRL = True |
---|
42 | |
---|
43 | try : |
---|
44 | import PIL.Image |
---|
45 | except ImportError : |
---|
46 | hasPIL = False |
---|
47 | else : |
---|
48 | hasPIL = True |
---|
49 | |
---|
50 | import pykota.appinit |
---|
51 | from pykota.utils import run |
---|
52 | from pykota.commandline import PyKotaOptionParser, \ |
---|
53 | checkandset_pagesize, \ |
---|
54 | checkandset_positiveint |
---|
55 | from pykota.pdfutils import getPageSize |
---|
56 | from pykota.errors import PyKotaToolError, PyKotaCommandLineError |
---|
57 | from pykota.tool import PyKotaTool |
---|
58 | from pykota.progressbar import Percent |
---|
59 | |
---|
60 | class PKRefund(PyKotaTool) : |
---|
61 | """A class for refund manager.""" |
---|
62 | validfilterkeys = [ "username", |
---|
63 | "printername", |
---|
64 | "hostname", |
---|
65 | "jobid", |
---|
66 | "billingcode", |
---|
67 | "start", |
---|
68 | "end", |
---|
69 | ] |
---|
70 | |
---|
71 | def printVar(self, label, value, size) : |
---|
72 | """Outputs a variable onto the PDF canvas. |
---|
73 | |
---|
74 | Returns the number of points to substract to current Y coordinate. |
---|
75 | """ |
---|
76 | xcenter = (self.pagesize[0] / 2.0) - 1*cm |
---|
77 | self.canvas.saveState() |
---|
78 | self.canvas.setFont("Helvetica-Bold", size) |
---|
79 | self.canvas.setFillColorRGB(0, 0, 0) |
---|
80 | self.canvas.drawRightString(xcenter, self.ypos, "%s :" % label) |
---|
81 | self.canvas.setFont("Courier-Bold", size) |
---|
82 | self.canvas.setFillColorRGB(0, 0, 1) |
---|
83 | self.canvas.drawString(xcenter + 0.5*cm, self.ypos, value) |
---|
84 | self.canvas.restoreState() |
---|
85 | self.ypos -= (size + 4) |
---|
86 | |
---|
87 | def pagePDF(self, receiptnumber, name, values, unit, reason) : |
---|
88 | """Generates a new page in the PDF document.""" |
---|
89 | if values["nbpages"] : |
---|
90 | self.canvas.doForm("background") |
---|
91 | self.ypos = self.yorigine - (cm + 20) |
---|
92 | self.printVar(_("Refunding receipt"), "#%s" % receiptnumber, 22) |
---|
93 | self.printVar(_("Username"), name, 22) |
---|
94 | self.ypos -= 20 |
---|
95 | datetime = time.strftime("%c", time.localtime()).decode(self.charset, "replace") |
---|
96 | self.printVar(_("Edited on"), datetime, 14) |
---|
97 | |
---|
98 | self.ypos -= 20 |
---|
99 | self.printVar(_("Jobs refunded"), str(values["nbjobs"]), 18) |
---|
100 | self.printVar(_("Pages refunded"), str(values["nbpages"]), 18) |
---|
101 | self.printVar(_("Amount refunded"), "%.3f %s" % (values["nbcredits"], unit), 18) |
---|
102 | self.ypos -= 20 |
---|
103 | self.printVar(_("Reason"), reason, 14) |
---|
104 | self.canvas.showPage() |
---|
105 | return 1 |
---|
106 | return 0 |
---|
107 | |
---|
108 | def initPDF(self, logo) : |
---|
109 | """Initializes the PDF document.""" |
---|
110 | self.pdfDocument = cStringIO.StringIO() |
---|
111 | self.canvas = c = canvas.Canvas(self.pdfDocument, \ |
---|
112 | pagesize=self.pagesize, \ |
---|
113 | pageCompression=1) |
---|
114 | |
---|
115 | c.setAuthor(self.effectiveUserName) |
---|
116 | c.setTitle(_("PyKota print job refunding receipts")) |
---|
117 | c.setSubject(_("Print job refunding receipts generated with PyKota")) |
---|
118 | |
---|
119 | self.canvas.beginForm("background") |
---|
120 | self.canvas.saveState() |
---|
121 | |
---|
122 | self.ypos = self.pagesize[1] - (2 * cm) |
---|
123 | |
---|
124 | xcenter = self.pagesize[0] / 2.0 |
---|
125 | if logo : |
---|
126 | try : |
---|
127 | imglogo = PIL.Image.open(logo) |
---|
128 | except IOError : |
---|
129 | self.printInfo("Unable to open image %s" % logo, "warn") |
---|
130 | else : |
---|
131 | (width, height) = imglogo.size |
---|
132 | multi = float(width) / (8 * cm) |
---|
133 | width = float(width) / multi |
---|
134 | height = float(height) / multi |
---|
135 | self.ypos -= height |
---|
136 | c.drawImage(logo, xcenter - (width / 2.0), \ |
---|
137 | self.ypos, \ |
---|
138 | width, height) |
---|
139 | |
---|
140 | self.ypos -= (cm + 20) |
---|
141 | self.canvas.setFont("Helvetica-Bold", 14) |
---|
142 | self.canvas.setFillColorRGB(0, 0, 0) |
---|
143 | msg = _("Here's the receipt for the refunding of your print jobs") |
---|
144 | self.canvas.drawCentredString(xcenter, self.ypos, "%s :" % msg) |
---|
145 | |
---|
146 | self.yorigine = self.ypos |
---|
147 | self.canvas.restoreState() |
---|
148 | self.canvas.endForm() |
---|
149 | |
---|
150 | def endPDF(self, fname) : |
---|
151 | """Flushes the PDF generator.""" |
---|
152 | self.canvas.save() |
---|
153 | if fname != "-" : |
---|
154 | outfile = open(fname, "w") |
---|
155 | outfile.write(self.pdfDocument.getvalue()) |
---|
156 | outfile.close() |
---|
157 | else : |
---|
158 | sys.stdout.write(self.pdfDocument.getvalue()) |
---|
159 | sys.stdout.flush() |
---|
160 | |
---|
161 | def genReceipts(self, peruser, logo, outfname, firstnumber, reason, unit) : |
---|
162 | """Generates the receipts file.""" |
---|
163 | if len(peruser) : |
---|
164 | percent = Percent(self, size=len(peruser)) |
---|
165 | if outfname != "-" : |
---|
166 | percent.display("%s...\n" % _("Generating receipts")) |
---|
167 | |
---|
168 | self.initPDF(logo) |
---|
169 | number = firstnumber |
---|
170 | for (name, values) in peruser.items() : |
---|
171 | number += self.pagePDF(number, name, values, unit, reason) |
---|
172 | if outfname != "-" : |
---|
173 | percent.oneMore() |
---|
174 | |
---|
175 | if number > firstnumber : |
---|
176 | self.endPDF(outfname) |
---|
177 | |
---|
178 | if outfname != "-" : |
---|
179 | percent.done() |
---|
180 | |
---|
181 | def main(self, arguments, options) : |
---|
182 | """Refunds jobs.""" |
---|
183 | if not hasRL : |
---|
184 | raise PyKotaToolError, "The ReportLab module is missing. Download it from http://www.reportlab.org" |
---|
185 | if not hasPIL : |
---|
186 | raise PyKotaToolError, "The Python Imaging Library is missing. Download it from http://www.pythonware.com/downloads" |
---|
187 | |
---|
188 | self.adminOnly() |
---|
189 | |
---|
190 | self.pagesize = getPageSize(options.pagesize) |
---|
191 | |
---|
192 | if (not options.reason) or (not options.reason.strip()) : |
---|
193 | raise PyKotaCommandLineError, _("Refunding for no reason is forbidden. Please use the --reason command line option.") |
---|
194 | |
---|
195 | extractonly = {} |
---|
196 | for filterexp in arguments : |
---|
197 | if filterexp.strip() : |
---|
198 | try : |
---|
199 | (filterkey, filtervalue) = [part.strip() for part in filterexp.split("=")] |
---|
200 | filterkey = filterkey.lower() |
---|
201 | if filterkey not in self.validfilterkeys : |
---|
202 | raise ValueError |
---|
203 | except ValueError : |
---|
204 | raise PyKotaCommandLineError, _("Invalid filter value [%s], see help.") % filterexp |
---|
205 | else : |
---|
206 | extractonly.update({ filterkey : filtervalue }) |
---|
207 | |
---|
208 | percent = Percent(self) |
---|
209 | outfname = options.output.strip().encode(sys.getfilesystemencoding()) |
---|
210 | if outfname != "-" : |
---|
211 | percent.display("%s..." % _("Extracting datas")) |
---|
212 | else : |
---|
213 | options.force = True |
---|
214 | self.printInfo(_("The PDF file containing the receipts will be sent to stdout. --force is assumed."), "warn") |
---|
215 | |
---|
216 | username = extractonly.get("username") |
---|
217 | if username : |
---|
218 | user = self.storage.getUser(username) |
---|
219 | else : |
---|
220 | user = None |
---|
221 | |
---|
222 | printername = extractonly.get("printername") |
---|
223 | if printername : |
---|
224 | printer = self.storage.getPrinter(printername) |
---|
225 | else : |
---|
226 | printer = None |
---|
227 | |
---|
228 | start = extractonly.get("start") |
---|
229 | end = extractonly.get("end") |
---|
230 | (start, end) = self.storage.cleanDates(start, end) |
---|
231 | |
---|
232 | jobs = self.storage.retrieveHistory(user=user, |
---|
233 | printer=printer, |
---|
234 | hostname=extractonly.get("hostname"), |
---|
235 | billingcode=extractonly.get("billingcode"), |
---|
236 | jobid=extractonly.get("jobid"), |
---|
237 | start=start, |
---|
238 | end=end, |
---|
239 | limit=0) |
---|
240 | try : |
---|
241 | loginname = os.getlogin() |
---|
242 | except OSError : |
---|
243 | loginname = pwd.getpwuid(os.getuid()).pw_name |
---|
244 | |
---|
245 | peruser = {} |
---|
246 | nbjobs = 0 |
---|
247 | nbpages = 0 |
---|
248 | nbcredits = 0.0 |
---|
249 | percent.setSize(len(jobs)) |
---|
250 | if outfname != "-" : |
---|
251 | percent.display("\n") |
---|
252 | for job in jobs : |
---|
253 | if job.JobSize and (job.JobAction not in ("DENY", "CANCEL", "REFUND")) : |
---|
254 | if options.info : |
---|
255 | reason = "%s : %s" % (options.info % { "nbpages" : job.JobSize, |
---|
256 | "nbcredits" : job.JobPrice, |
---|
257 | "effectiveuser" : self.effectiveUserName, |
---|
258 | "loginname" : loginname, |
---|
259 | "date" : str(DateTime.now())[:19], |
---|
260 | }, |
---|
261 | options.reason) |
---|
262 | else : |
---|
263 | reason = options.reason |
---|
264 | |
---|
265 | if options.force : |
---|
266 | nbpages += job.JobSize |
---|
267 | nbcredits += job.JobPrice |
---|
268 | counters = peruser.setdefault(job.UserName, |
---|
269 | { "nbjobs" : 0, |
---|
270 | "nbpages" : 0, |
---|
271 | "nbcredits" : 0.0, |
---|
272 | }) |
---|
273 | counters["nbpages"] += job.JobSize |
---|
274 | counters["nbcredits"] += job.JobPrice |
---|
275 | job.refund(reason) |
---|
276 | counters["nbjobs"] += 1 |
---|
277 | nbjobs += 1 |
---|
278 | if outfname != "-" : |
---|
279 | percent.oneMore() |
---|
280 | else : |
---|
281 | self.display("%s\n" % (_("Date : %s") % str(job.JobDate)[:19])) |
---|
282 | self.display("%s\n" % (_("Printer : %s") % job.PrinterName)) |
---|
283 | self.display("%s\n" % (_("User : %s") % job.UserName)) |
---|
284 | self.display("%s\n" % (_("JobId : %s") % job.JobId)) |
---|
285 | self.display("%s\n" % (_("Title : %s") % job.JobTitle)) |
---|
286 | if job.JobBillingCode : |
---|
287 | self.display("%s\n" % (_("Billing code : %s") % job.JobBillingCode)) |
---|
288 | self.display("%s\n" % (_("Pages : %i") % job.JobSize)) |
---|
289 | self.display("%s\n" % (_("Credits : %.3f") % job.JobPrice)) |
---|
290 | |
---|
291 | while True : |
---|
292 | answer = raw_input("\t%s ? " % _("Refund (Y/N)")).strip().upper() |
---|
293 | if answer == _("Y") : |
---|
294 | nbpages += job.JobSize |
---|
295 | nbcredits += job.JobPrice |
---|
296 | counters = peruser.setdefault(job.UserName, |
---|
297 | { "nbjobs" : 0, |
---|
298 | "nbpages" : 0, |
---|
299 | "nbcredits" : 0.0, |
---|
300 | }) |
---|
301 | counters["nbpages"] += job.JobSize |
---|
302 | counters["nbcredits"] += job.JobPrice |
---|
303 | job.refund(reason) |
---|
304 | counters["nbjobs"] += 1 |
---|
305 | nbjobs += 1 |
---|
306 | break |
---|
307 | elif answer == _("N") : |
---|
308 | break |
---|
309 | sys.stdout.write("\n") |
---|
310 | if outfname != "-" : |
---|
311 | percent.done() |
---|
312 | self.genReceipts(peruser, |
---|
313 | options.logo.strip().encode(sys.getfilesystemencoding()), |
---|
314 | outfname, |
---|
315 | options.number, |
---|
316 | options.reason, # We don't want the full reason here. |
---|
317 | options.unit or _("Credits")) |
---|
318 | if outfname != "-" : |
---|
319 | nbusers = len(peruser) |
---|
320 | self.display("%s\n" % (_("Refunded %(nbusers)i users for %(nbjobs)i jobs, %(nbpages)i pages and %(nbcredits).3f credits") \ |
---|
321 | % locals())) |
---|
322 | |
---|
323 | if __name__ == "__main__" : |
---|
324 | parser = PyKotaOptionParser(description=_("Refunding tool for PyKota."), |
---|
325 | usage="pkrefund [options] [filterexpr]") |
---|
326 | parser.add_option("-f", "--force", |
---|
327 | dest="force", |
---|
328 | action="store_true", |
---|
329 | help=_("Doesn't ask for confirmation before refunding. Only needed if you specify a filename for the PDF receipts. If you send such receipts to stdout, --force is assumed to be set.")) |
---|
330 | parser.add_option("-i", "--info", |
---|
331 | dest="info", |
---|
332 | type="string", |
---|
333 | default=_("Refunded %(nbpages)i pages and %(nbcredits).3f credits by %(effectiveuser)s (%(loginname)s) on %(date)s"), |
---|
334 | help=_("The informations to be prepended to the refunding reason, which can include some of Python's string interpolations to access to certain internal values. If you don't want such information, set this explicitely to an empty string. The default is '''%default'''")) |
---|
335 | parser.add_option("-l", "--logo", |
---|
336 | dest="logo", |
---|
337 | default=u"/usr/share/pykota/logos/pykota.jpeg", |
---|
338 | help=_("The image to use as a logo. The logo will be drawn at the center top of the page. The default logo is %default.")) |
---|
339 | parser.add_option("-n", "--number", |
---|
340 | dest="number", |
---|
341 | type="int", |
---|
342 | action="callback", |
---|
343 | callback=checkandset_positiveint, |
---|
344 | default=1, |
---|
345 | help=_("Sets the number of the first receipt. This number will automatically be incremented for each receipt. The default value is %default.")) |
---|
346 | parser.add_option("-o", "--output", |
---|
347 | dest="output", |
---|
348 | type="string", |
---|
349 | default=u"-", |
---|
350 | help=_("The name of the file to which the PDF receipts will be written. If not set or set to '%default', the PDF document will be sent to the standard output, and --force will be assumed to be set.")) |
---|
351 | parser.add_option("-p", "--pagesize", |
---|
352 | type="string", |
---|
353 | action="callback", |
---|
354 | callback=checkandset_pagesize, |
---|
355 | dest="pagesize", |
---|
356 | default=u"A4", |
---|
357 | help=_("Set the size of the page. Most well known page sizes are recognized, like 'A4' or 'Letter' to name a few. The default page size is %default.")) |
---|
358 | parser.add_option("-r", "--reason", |
---|
359 | dest="reason", |
---|
360 | type="string", |
---|
361 | help=_("The reason why there was a refund.")) |
---|
362 | |
---|
363 | # TODO : due to Python's optparse.py bug #1498146 fixed in rev 46861 |
---|
364 | # TODO : we can't use 'default=_("Credits")' for this option |
---|
365 | parser.add_option("-u", "--unit", |
---|
366 | dest="unit", |
---|
367 | type="string", |
---|
368 | help=_("The name of the unit to use on the receipts. The default value is 'Credits' or its locale translation.")) |
---|
369 | |
---|
370 | parser.add_filterexpression("username", _("User's name")) |
---|
371 | parser.add_filterexpression("printername", _("Printer's name")) |
---|
372 | parser.add_filterexpression("hostname", _("Host's name")) |
---|
373 | parser.add_filterexpression("jobid", _("Job's id")) |
---|
374 | parser.add_filterexpression("billingcode", _("Job's billing code")) |
---|
375 | parser.add_filterexpression("start", _("Job's date of printing")) |
---|
376 | parser.add_filterexpression("end", _("Job's date of printing")) |
---|
377 | |
---|
378 | parser.add_example('--output /tmp/receipts.pdf jobid=503', |
---|
379 | _("This would refund all jobs which Id is 503. A confirmation would be asked for each job to refund, and a PDF file named /tmp/receipts.pdf would be created containing printable receipts. BEWARE of job ids rolling over if you reset CUPS' history.")) |
---|
380 | |
---|
381 | parser.add_example('--reason "Hardware problem" jobid=503 start=today-7', |
---|
382 | _("This would refund all jobs which id is 503 but which would have been printed during the past week. The reason would be marked as being an hardware problem.")) |
---|
383 | |
---|
384 | parser.add_example('--force username=jerome printername=HP2100', |
---|
385 | _("This would refund all jobs printed by user jerome on printer HP2100. No confirmation would be asked.")) |
---|
386 | |
---|
387 | parser.add_example('--force printername=HP2100 start=200602 end=yesterday', |
---|
388 | _("This would refund all jobs printed on printer HP2100 between February 1st 2006 and yesterday. No confirmation would be asked.")) |
---|
389 | |
---|
390 | (options, arguments) = parser.parse_args() |
---|
391 | run(parser, PKRefund) |
---|