No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

freitagskasse.py 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. #! /usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. #
  4. # Copyright (C) 2011 Sebastian Pipping <sebastian@pipping.org>
  5. # Licensed under GPL v3 or later
  6. from __future__ import print_function
  7. import freitagslib.network as net
  8. from freitagslib.commands import BuyCommand, DepositCommand
  9. import colorama
  10. from colorama import Fore, Style
  11. import sys
  12. from decimal import Decimal
  13. import os
  14. import time
  15. import urllib2
  16. from thread import start_new_thread, allocate_lock
  17. import time
  18. COLOR_HINT = Fore.YELLOW + Style.BRIGHT
  19. COLOR_ERROR = Fore.RED
  20. COLOR_DEPOSIT = Fore.GREEN + Style.BRIGHT
  21. COLOR_TO_GO = Fore.MAGENTA + Style.BRIGHT
  22. COLOR_WITHDRAW = Fore.RED + Style.BRIGHT
  23. COLOR_SOME = Fore.WHITE + Style.BRIGHT
  24. COLOR_MUCH = Fore.YELLOW + Style.BRIGHT
  25. COLOR_RESET = Style.RESET_ALL
  26. display_fifo = None
  27. scroll_line1 = None
  28. scroll_line2 = None
  29. idlemessage = None
  30. offset_line1 = 0
  31. offset_line2 = 0
  32. brightness = 5
  33. screensaver = 0
  34. SCREENSAVER_DIM = 30
  35. SCREENSAVER_TIMEOUT = 90
  36. SCREENSAVER_OFF = 300
  37. lock = allocate_lock()
  38. #Helper-Function, scrolls text in a specific line.
  39. def display_scroll_text(line,text):
  40. global scroll_line1,scroll_line2
  41. global offset_line1,offset_line2
  42. lock.acquire()
  43. if (line==1): #clear the line on invocation:
  44. send_display("\x1f\x24\x01%c\x18" % chr(line) )
  45. scroll_line1 = text
  46. offset_line1=0
  47. if (line==2): #clear the line on invocation:
  48. send_display("\x1f\x24\x01%c\x18" % chr(line) )
  49. scroll_line2 = text
  50. offset_line2=0
  51. lock.release()
  52. def display_handle_scroll(line,text,offset):
  53. if (text):
  54. l = len(text)
  55. if (l<21):
  56. if (offset == 0):
  57. send_display("\x1f\x24\x01%c%s" % (chr(line),text[offset:(20+offset)]))
  58. offset=1
  59. else:
  60. send_display("\x1f\x24\x01%c%s" % (chr(line),text[offset:(20+offset)]))
  61. missing_chars=20+offset-l
  62. if (missing_chars>0):
  63. send_display(text[:missing_chars])
  64. offset=((offset+1)%l)
  65. else:
  66. offset=0 #reset offset
  67. return offset
  68. def display_cmd_dim(x):
  69. global brightness
  70. if (brightness!=x):
  71. if (x==0): #turn off:
  72. send_display("\x1F\x45%c" % chr(255) )
  73. brightness = x
  74. return
  75. else:
  76. if (brightness==0): #turn on, then set wanted brightness:
  77. send_display("\x1F\x45%c" % chr(0) )
  78. brightness = x
  79. send_display("\x1F\x58%c" % chr(x-1) )
  80. def display_thread(x):
  81. global scroll_line1,scroll_line2
  82. global offset_line1,offset_line2
  83. global screensaver
  84. global idlemessage
  85. offset_line1=0
  86. offset_line2=0
  87. while(True):
  88. lock.acquire()
  89. offset_line1 = display_handle_scroll(1,scroll_line1,offset_line1)
  90. offset_line2 = display_handle_scroll(2,scroll_line2,offset_line2)
  91. if (screensaver <= SCREENSAVER_OFF):
  92. screensaver=screensaver+1
  93. if ((screensaver >= SCREENSAVER_DIM) and (screensaver <= (SCREENSAVER_DIM+7))): #activate first stage of screensaver:
  94. x = (8-( screensaver - SCREENSAVER_DIM))/2
  95. display_cmd_dim(1+x)
  96. if (screensaver == SCREENSAVER_TIMEOUT):
  97. now = time.localtime()
  98. send_display("\x0c\x1F\x54%c%c\x1f\x03" % (chr(now.tm_hour),chr(now.tm_min)));
  99. if (scroll_line2):
  100. scroll_line1=scroll_line2
  101. else:
  102. scroll_line1=idlemessage
  103. scroll_line2=None
  104. offset_line1=0
  105. offset_line2=0
  106. if (screensaver == SCREENSAVER_OFF):
  107. display_cmd_dim(0)
  108. lock.release()
  109. time.sleep(.5)
  110. def send_display(s):
  111. global display_fifo
  112. if not display_fifo:
  113. try:
  114. fd = os.open( "/tmp/display", os.O_WRONLY)
  115. display_fifo = os.fdopen(fd,"w")
  116. except OSError:
  117. display_fifo = None
  118. pass
  119. return
  120. except IOError:
  121. display_fifo = None
  122. pass
  123. return
  124. try:
  125. display_fifo.write(s)
  126. display_fifo.flush()
  127. except IOError:
  128. display_fifo = None
  129. pass
  130. return
  131. #Front-End Funtion to display a Screen
  132. # with heading
  133. def display_screen(title,message):
  134. global scroll_line1,scroll_line2
  135. global offset_line1,offset_line2
  136. global screensaver
  137. offset_line1=0
  138. offset_line2=0
  139. screensaver=0
  140. display_cmd_dim(5)
  141. if (len(title)<21):
  142. scroll_line1=None
  143. send_display("\x1f\x24\x01%c\x18%s" % (chr(1),'\xdb'*20) )
  144. if (len(title)<20):
  145. pos=1+(20-len(title))/2
  146. else:
  147. pos=1
  148. send_display("\x1f\x24%c%c%s" % (chr(pos),chr(1),title) )
  149. else:
  150. display_scroll_text(1,title)
  151. display_scroll_text(2,message)
  152. #Front-End function to send data to the display.
  153. def print_display(s):
  154. global scroll_line1,scroll_line2
  155. global offset_line1,offset_line2
  156. global screensaver
  157. lock.acquire()
  158. screensaver=0
  159. offset_line1=0
  160. offset_line2=0
  161. scroll_line1=None
  162. scroll_line2=None
  163. display_cmd_dim(5)
  164. send_display(s)
  165. lock.release()
  166. def clear():
  167. os.system('clear')
  168. TO_GO_ONLY = 2
  169. TO_GO_ALL = 1
  170. TO_GO_NONE = 0
  171. TO_GO_PREV = -1
  172. CODES = {
  173. 'UNDO':('undo',),
  174. 'COMMIT':('commit',),
  175. 'TO GO ALL':('to_go', TO_GO_ALL),
  176. 'TO GO NONE':('to_go', TO_GO_NONE),
  177. 'TO GO PREV':('to_go', TO_GO_PREV),
  178. 'TO GO ONLY':('to_go', TO_GO_ONLY),
  179. 'DEPOSIT 0.01':('deposit', Decimal('0.01')),
  180. 'DEPOSIT 0.05':('deposit', Decimal('0.05')),
  181. 'DEPOSIT 0.10':('deposit', Decimal('0.10')),
  182. 'DEPOSIT 0.50':('deposit', Decimal('0.50')),
  183. 'DEPOSIT 1.00':('deposit', Decimal('1.00')),
  184. 'DEPOSIT 5.00':('deposit', Decimal('5.00')),
  185. 'DEPOSIT 10.00':('deposit', Decimal('10.00')),
  186. 'DEPOSIT 50.00':('deposit', Decimal('50.00')),
  187. }
  188. def delay(what, seconds):
  189. for i in xrange(seconds, 0, -1):
  190. if i < seconds:
  191. sys.stdout.write('\r')
  192. sys.stdout.write('%s in %d Sekunden... ' % (what, i))
  193. sys.stdout.flush()
  194. time.sleep(1)
  195. def warn_balance():
  196. print('Kontostand im Minus, bitte Geld aufladen.')
  197. def error_page(error_message, hint_message=None):
  198. clear()
  199. print(COLOR_ERROR + error_message + COLOR_RESET)
  200. print()
  201. delay_seconds = 3
  202. if hint_message is not None:
  203. print(COLOR_HINT + hint_message + COLOR_RESET)
  204. print()
  205. delay_seconds += 3
  206. delay('Weiter', delay_seconds)
  207. class Status:
  208. def __init__(self):
  209. self._reset()
  210. self.item_cache = dict()
  211. def _reset(self):
  212. self.auth_blob = None
  213. self.login_name = None
  214. self.balance = None
  215. self.transfers = None
  216. def dump(self):
  217. def sign(amount, plus='+'):
  218. return '-' if amount < 0 else plus
  219. def color(amount):
  220. return COLOR_WITHDRAW if amount < 0 else COLOR_DEPOSIT
  221. def show_total(balance, plus=' '):
  222. print('%3s %-40s %s%c %6.2f Euro%s' \
  223. % ('', '', color(balance), sign(balance, plus),
  224. abs(balance), COLOR_RESET))
  225. def show_item(position, diff, label, color):
  226. print('%2d) %-40s %s%c %6.2f Euro%s' \
  227. % (position, label, color, sign(diff),
  228. abs(diff), COLOR_RESET))
  229. def show_bar():
  230. print('%3s %-40s %13s' % ('', '', '============='))
  231. if self.logged_in():
  232. print('Eingeloggt als: %s%s%s' % (COLOR_SOME, self.login_name, COLOR_RESET))
  233. print()
  234. print_display('\x0cHallo %-14s' % (self.login_name[:13]+"!") )
  235. if self.transfers:
  236. initial_command, initial_balance = self.transfers[0]
  237. print('Geplante Änderungen:')
  238. show_total(initial_balance)
  239. i = 1
  240. for command, dummy in self.transfers:
  241. if isinstance(command, BuyCommand):
  242. if (command.includes_commodity()):
  243. show_item(i, -command.commodity_value(), command.commodity_label(), COLOR_WITHDRAW)
  244. i += 1
  245. if (command.includes_deposit()):
  246. show_item(i, -command.deposit_value(), command.deposit_label(), COLOR_TO_GO)
  247. i += 1
  248. else:
  249. show_item(i, command.difference(), command.label(), COLOR_DEPOSIT)
  250. i += 1
  251. show_bar()
  252. if isinstance(command, BuyCommand):
  253. mycmd = 0;
  254. if (command.includes_commodity()):
  255. mycmd+=1;
  256. if (command.includes_deposit()):
  257. mycmd+=2;
  258. if (mycmd==1):
  259. mylabel=command.item_name()
  260. if (mycmd==2):
  261. mylabel="Pfand "+command.commodity_label()[:9]
  262. if (mycmd==3):
  263. mylabel=("%-13s" % (command.commodity_label()[:13]))+"+P"
  264. print_display('\x0b%-15s %4.2f' % (mylabel[:15],abs(command.difference())));
  265. print_display('\x0b\nSUMME: {%02i} %8.2f' % ((i-1),initial_balance - self.balance));
  266. else:
  267. print_display('\x0b%-15s %4.2f' % (command.label()[:15],abs(command.difference())));
  268. print_display('\x0b\nSUMME: {%02i} %8.2f' % ((i-1),initial_balance - self.balance));
  269. if len(self.transfers) > 1:
  270. show_total(self.balance - initial_balance, plus='+')
  271. show_bar()
  272. show_total(self.balance)
  273. if self.balance < 0:
  274. warn_balance()
  275. print_display('\x0b\nKonto: %5.2f!' % (self.balance) )
  276. print()
  277. print(COLOR_SOME + 'Committen nicht vergessen.' + COLOR_RESET)
  278. else:
  279. print('Kontostand beträgt: %s%.2f Euro%s' % (COLOR_MUCH, self.balance, COLOR_RESET))
  280. display_screen("KONTOSTAND","%s: %.2f Euro" % (self.login_name,self.balance))
  281. if self.balance < 0:
  282. print()
  283. warn_balance()
  284. else:
  285. print(COLOR_MUCH + 'Bitte einloggen.' + COLOR_RESET)
  286. print()
  287. print('Scanne dazu deine ID-Karte mit dem Barcode-Leser.')
  288. display_screen("LOGIN","Bitte scanne Deinen login-Token! *** ")
  289. print()
  290. def logged_in(self):
  291. return self.auth_blob is not None
  292. def login(self, auth_blob):
  293. assert(not self.logged_in())
  294. user_name = net.get_user_name_from_auth_blob(auth_blob)
  295. balance = net.get_balance(user_name)
  296. self.auth_blob = auth_blob
  297. self.login_name = user_name
  298. self.balance = balance
  299. self.transfers = list()
  300. def commit(self):
  301. assert(self.logged_in())
  302. # Process command queue
  303. for (command, balance_backup) in list(self.transfers):
  304. try:
  305. command.run(self.login_name)
  306. except urllib2.HTTPError as e:
  307. print_display('\x0cFEHLER: Server Error%20s' % str(e)[:20])
  308. error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e))
  309. break
  310. else:
  311. self.transfers.pop(0)
  312. if not self.transfers:
  313. # Show final balance for some time
  314. clear()
  315. self.dump()
  316. print_display('\x0bDanke, %-12s' % (self.login_name[:12]+"!") )
  317. delay('Logout', 3)
  318. # Logout
  319. self._reset()
  320. def find(self, barcode):
  321. try:
  322. return self.item_cache[barcode]
  323. except KeyError:
  324. item = net.get_item(barcode)
  325. self.item_cache[barcode] = item
  326. return item
  327. def buy(self, item):
  328. assert(self.logged_in())
  329. log_entry = (BuyCommand(item), self.balance)
  330. self.transfers.append(log_entry)
  331. self.balance = self.balance - item.price
  332. def deposit(self, amount):
  333. assert(self.logged_in())
  334. log_entry = (DepositCommand(amount), self.balance)
  335. self.transfers.append(log_entry)
  336. self.balance = self.balance + amount
  337. def to_go(self, scope):
  338. _PRODUCT_FIRST = 'FEHLER: Bitte zuerst den betreffenden Artikel scannen.'
  339. if not self.transfers:
  340. error_page(_PRODUCT_FIRST)
  341. return
  342. if scope == TO_GO_ALL:
  343. """
  344. Makes all BuyCommands with deposit > 0 include deposit
  345. ...and updates future balance accordingly.
  346. """
  347. dummy, initial_balance = self.transfers[0]
  348. balance_before = initial_balance
  349. for command, dummy in self.transfers:
  350. if isinstance(command, BuyCommand) \
  351. and not command.includes_deposit() \
  352. and command.deposit_value() > 0:
  353. command.include_deposit(True)
  354. balance_before += command.difference()
  355. self.balance = balance_before
  356. elif scope == TO_GO_NONE:
  357. """
  358. Makes all BuyCommands that include commodity
  359. not include deposit any more
  360. ...and updates future balance accordingly.
  361. """
  362. first_command, initial_balance = self.transfers[0]
  363. balance_before = initial_balance
  364. for command, dummy in self.transfers:
  365. if isinstance(command, BuyCommand) \
  366. and command.includes_commodity():
  367. command.include_deposit(False)
  368. balance_before += command.difference()
  369. self.balance = balance_before
  370. elif scope == TO_GO_PREV:
  371. """
  372. Makes the last BuyCommand include deposit
  373. ...and updates future balance accordingly.
  374. """
  375. prev, balance_backup = self.transfers[-1]
  376. if not isinstance(prev, BuyCommand):
  377. error_page(_PRODUCT_FIRST)
  378. return
  379. if prev.includes_deposit():
  380. print_display('\x0cFEHLER: schon Pfand %20s' % prev.item_name()[:20])
  381. error_page('FEHLER: Pfand für Produkt "%s" bereits aktiviert' % prev.item_name())
  382. return
  383. if prev.deposit_value() <= 0:
  384. print_display('\x0cFEHLER: Pfandfrei! %20s' % prev.item_name()[:20])
  385. error_page('FEHLER: Produkt "%s" hat kein Pfand' % prev.item_name())
  386. return
  387. before = prev.difference()
  388. prev.include_deposit(True)
  389. after = prev.difference()
  390. self.balance += (after - before)
  391. elif scope == TO_GO_ONLY:
  392. """
  393. Makes all BuyCommand that include commodity
  394. be deposit only
  395. ...and updates future balance accordingly.
  396. """
  397. dummy, initial_balance = self.transfers[0]
  398. balance_before = initial_balance
  399. for command, dummy in self.transfers:
  400. if isinstance(command, BuyCommand) \
  401. and command.deposit_value() > 0 \
  402. and command.includes_commodity():
  403. command.include_commodity(False)
  404. command.include_deposit(True)
  405. balance_before += command.difference()
  406. self.balance = balance_before
  407. def undo(self):
  408. assert(self.logged_in())
  409. if self.transfers:
  410. dummy, balance_backup = self.transfers[-1]
  411. self.transfers.pop()
  412. self.balance = balance_backup
  413. else:
  414. error_page('FEHLER: Nichts da, was ich rückgängig machen könnte.')
  415. def print_prompt():
  416. sys.stdout.write(">>> ")
  417. def handle(line, status):
  418. global idlemessage
  419. if line == 'exit':
  420. clear()
  421. print_display("\x0c")
  422. sys.exit(0)
  423. if status.logged_in():
  424. idlemessage=" Comitten nicht vergessen! ***"
  425. if line in CODES:
  426. call = CODES[line]
  427. method = call[0]
  428. params = call[1:]
  429. getattr(status, method)(*params)
  430. else:
  431. try:
  432. item = status.find(line)
  433. except urllib2.HTTPError as e:
  434. if e.code == 404: # URL not found == item not found with REST
  435. print_display('\x0cERROR: %13sCode ist unbekannt' % ( line[:13]))
  436. error_page('FEHLER: Aktion oder Ware "%s" nicht bekannt' % line)
  437. else:
  438. print_display('\x0cERROR: Server Error%20s' % str(e)[:20])
  439. error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e))
  440. else:
  441. status.buy(item)
  442. else:
  443. idlemessage="Mir ist langweilig!"
  444. try:
  445. status.login(line)
  446. except urllib2.HTTPError as e:
  447. if e.code == 404: # URL not found == user unknown
  448. display_screen("FEHLER","Nutzer ist unbekannt: '%s' *** " % line)
  449. error_page('FEHLER: Benutzer "%s" nicht bekannt' % line,
  450. hint_message='Ist in der WebApp unter "Einstellungen" ' \
  451. 'für Ihren Account Plugin "BarcodePlugin" ' \
  452. 'als erlaubt markiert?')
  453. else:
  454. print_display('\x0cFEHLER: Server Error%20s' % str(e)[:20])
  455. error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e))
  456. except urllib2.URLError as e:
  457. error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e))
  458. def main():
  459. colorama.init()
  460. status = Status()
  461. global scroll_line1,scroll_line2
  462. print_display("\x1b\x40\x1f\x43\x00")
  463. start_new_thread(display_thread,(1,))
  464. #display_scroll_text(1,"Line1: Text, dumm scrollend!")
  465. #display_scroll_text(4,"Line2: Und hier Text, auch dumm scrollend!")
  466. display_screen("HINWEIS","Herzlich willkommen bei der Freitagsrunde! *** ")
  467. while True:
  468. clear()
  469. status.dump()
  470. print_prompt()
  471. l = sys.stdin.readline()
  472. if not l:
  473. break
  474. line = l.rstrip()
  475. print_display('\x0cBarcode:\n%20s' % line[:20])
  476. if line:
  477. handle(line, status)
  478. if __name__ == '__main__':
  479. try:
  480. main()
  481. except KeyboardInterrupt:
  482. pass
  483. print_display("\x1b\x40Goodbye!")