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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  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. from freitagslib.encoding import asciify
  10. import colorama
  11. from colorama import Fore, Style
  12. import sys
  13. from decimal import Decimal
  14. import os
  15. import time
  16. import urllib2
  17. import select
  18. from display import Display
  19. from thread import start_new_thread, allocate_lock
  20. import time
  21. myDisplay=None
  22. COLOR_HINT = Fore.YELLOW + Style.BRIGHT
  23. COLOR_ERROR = Fore.RED
  24. COLOR_DEPOSIT = Fore.GREEN + Style.BRIGHT
  25. COLOR_TO_GO = Fore.MAGENTA + Style.BRIGHT
  26. COLOR_WITHDRAW = Fore.RED + Style.BRIGHT
  27. COLOR_SOME = Fore.WHITE + Style.BRIGHT
  28. COLOR_MUCH = Fore.YELLOW + Style.BRIGHT
  29. COLOR_RESET = Style.RESET_ALL
  30. display_fifo = None
  31. scroll_line1 = None
  32. scroll_line2 = None
  33. offset_line1 = 0
  34. offset_line2 = 0
  35. brightness = 5
  36. screensaver = 0
  37. SCREENSAVER_DIM = 2* 20
  38. SCREENSAVER_TIMEOUT = 2* 120
  39. SCREENSAVER_OFF = 2* 10*60
  40. lock = allocate_lock()
  41. def clear():
  42. os.system('clear')
  43. TO_GO_ONLY = 2
  44. TO_GO_ALL = 1
  45. TO_GO_NONE = 0
  46. TO_GO_PREV = -1
  47. CODES = {
  48. 'UNDO':('undo',),
  49. 'COMMIT':('commit',),
  50. 'TO GO ALL':('to_go', TO_GO_ALL),
  51. 'TO GO NONE':('to_go', TO_GO_NONE),
  52. 'TO GO PREV':('to_go', TO_GO_PREV),
  53. 'TO GO ONLY':('to_go', TO_GO_ONLY),
  54. 'TO GO ONLZ':('to_go', TO_GO_ONLY), # Workaround for German keyboard layouts
  55. 'DEPOSIT 0.01':('deposit', Decimal('0.01')),
  56. 'DEPOSIT 0.05':('deposit', Decimal('0.05')),
  57. 'DEPOSIT 0.10':('deposit', Decimal('0.10')),
  58. 'DEPOSIT 0.50':('deposit', Decimal('0.50')),
  59. 'DEPOSIT 1.00':('deposit', Decimal('1.00')),
  60. 'DEPOSIT 5.00':('deposit', Decimal('5.00')),
  61. 'DEPOSIT 10.00':('deposit', Decimal('10.00')),
  62. 'DEPOSIT 50.00':('deposit', Decimal('50.00')),
  63. }
  64. def delay(what, seconds):
  65. for i in xrange(seconds, 0, -1):
  66. if i < seconds:
  67. sys.stdout.write('\r')
  68. sys.stdout.write('%s in %d Sekunden... ' % (what, i))
  69. sys.stdout.flush()
  70. time.sleep(1)
  71. def warn_balance():
  72. print('Kontostand im Minus, bitte Geld aufladen.')
  73. def error_page(error_message, hint_message=None):
  74. clear()
  75. print(COLOR_ERROR + error_message + COLOR_RESET)
  76. print()
  77. delay_seconds = 3
  78. if hint_message is not None:
  79. print(COLOR_HINT + hint_message + COLOR_RESET)
  80. print()
  81. delay_seconds += 3
  82. delay('Weiter', delay_seconds)
  83. def item_info_page(item):
  84. indent = 4 * ' '
  85. clear()
  86. print('Diese Ware heißt')
  87. print()
  88. print(indent + COLOR_SOME + item.name + COLOR_RESET)
  89. print()
  90. print('und kostet')
  91. print()
  92. if item.deposit > 0:
  93. myDisplay.display_screen("PREISINFO","%s: %4.2f Euro (%4.2f Euro Pfand)" % (asciify(item.name), item.price, item.deposit))
  94. print(indent + '%s%4.2f Euro%s + %4.2f Euro Pfand = %s%4.2f Euro%s .' \
  95. % (COLOR_SOME, item.price, COLOR_RESET, item.deposit,
  96. COLOR_MUCH, item.price + item.deposit, COLOR_RESET))
  97. else:
  98. myDisplay.display_screen("PREISINFO","%s: %4.2f Euro" % (asciify(item.name), item.price))
  99. print(indent + '%s%4.2f Euro%s .' \
  100. % (COLOR_MUCH, item.price, COLOR_RESET))
  101. print()
  102. print()
  103. print(COLOR_MUCH + 'Zum Kaufen bitte einloggen.' + COLOR_RESET)
  104. print()
  105. delay('Weiter', 6)
  106. class Status:
  107. def __init__(self):
  108. self._reset()
  109. self.item_cache = dict()
  110. def _reset(self):
  111. self.auth_blob = None
  112. self.login_name = None
  113. self.balance = None
  114. self.transfers = None
  115. def dump(self):
  116. def sign(amount, plus='+'):
  117. return '-' if amount < 0 else plus
  118. def color(amount):
  119. return COLOR_WITHDRAW if amount < 0 else COLOR_DEPOSIT
  120. def show_total(balance, plus=' '):
  121. print('%3s %-40s %s%c %6.2f Euro%s' \
  122. % ('', '', color(balance), sign(balance, plus),
  123. abs(balance), COLOR_RESET))
  124. def shorten(text, length):
  125. if len(text) <= length:
  126. return text
  127. else:
  128. return text[:length - 3] + '...'
  129. def show_item(position, diff, label, color):
  130. print('%2d) %-40s %s%c %6.2f Euro%s' \
  131. % (position, shorten(label, 40), color, sign(diff),
  132. abs(diff), COLOR_RESET))
  133. def show_bar():
  134. print('%3s %-40s %13s' % ('', '', '============='))
  135. if self.logged_in():
  136. print('Eingeloggt als: %s%s%s' % (COLOR_SOME, self.login_name, COLOR_RESET))
  137. print()
  138. myDisplay.cmd_clear()
  139. myDisplay.write('Hallo %-14s' % (self.login_name[:13]+"!") )
  140. if self.transfers:
  141. initial_command, initial_balance = self.transfers[0]
  142. print('Geplante Änderungen:')
  143. show_total(initial_balance)
  144. i = 1
  145. for command, dummy in self.transfers:
  146. if isinstance(command, BuyCommand):
  147. if (command.includes_commodity()):
  148. show_item(i, -command.commodity_value(), command.commodity_label(), COLOR_WITHDRAW)
  149. i += 1
  150. if (command.includes_deposit()):
  151. show_item(i, -command.deposit_value(), command.deposit_label(), COLOR_TO_GO)
  152. i += 1
  153. else:
  154. show_item(i, command.difference(), command.label(), COLOR_DEPOSIT)
  155. i += 1
  156. show_bar()
  157. if isinstance(command, BuyCommand):
  158. mycmd = 0;
  159. if (command.includes_commodity()):
  160. mycmd+=1;
  161. if (command.includes_deposit()):
  162. mycmd+=2;
  163. if (mycmd==1):
  164. mylabel=command.item_name()
  165. if (mycmd==2):
  166. mylabel="Pfand "+command.commodity_label()[:9]
  167. if (mycmd==3):
  168. mylabel=("%-13s" % (command.commodity_label()[:13]))+"+P"
  169. myDisplay.cmd_home()
  170. myDisplay.write('%-15s %4.2f' % (asciify(mylabel)[:15],abs(command.difference())));
  171. myDisplay.cmd_home()
  172. myDisplay.write('\nSUMME: {%02i} %8.2f' % ((i-1),initial_balance - self.balance));
  173. else:
  174. myDisplay.cmd_home()
  175. myDisplay.write('%-15s %4.2f' % (command.label()[:15],abs(command.difference())));
  176. myDisplay.cmd_home()
  177. myDisplay.write('\nSUMME: {%02i} %8.2f' % ((i-1),initial_balance - self.balance));
  178. if len(self.transfers) > 1:
  179. show_total(self.balance - initial_balance, plus='+')
  180. show_bar()
  181. show_total(self.balance)
  182. if self.balance < 0:
  183. warn_balance()
  184. myDisplay.cmd_home()
  185. myDisplay.write('\nKonto: %5.2f!' % (self.balance) )
  186. print()
  187. print(COLOR_MUCH + 'Committen nicht vergessen.' + COLOR_RESET)
  188. else:
  189. print('Kontostand beträgt: %s%.2f Euro%s' % (COLOR_MUCH, self.balance, COLOR_RESET))
  190. myDisplay.display_screen("KONTOSTAND","%s: %.2f Euro" % (self.login_name,self.balance))
  191. if self.balance < 0:
  192. print()
  193. warn_balance()
  194. else:
  195. print(COLOR_MUCH + 'Bitte einloggen.' + COLOR_RESET)
  196. print()
  197. print('Scanne dazu deine ID-Karte mit dem Barcode-Leser.')
  198. myDisplay.display_screen("LOGIN","Bitte scanne Deinen Login-Token! *** ")
  199. print()
  200. def logged_in(self):
  201. return self.auth_blob is not None
  202. def login(self, auth_blob):
  203. assert(not self.logged_in())
  204. user_name = net.get_user_name_from_auth_blob(auth_blob)
  205. balance = net.get_balance(user_name)
  206. self.auth_blob = auth_blob
  207. self.login_name = user_name
  208. self.balance = balance
  209. self.transfers = list()
  210. def logout(self):
  211. # Must not fail if not logged in
  212. self._reset()
  213. def commit(self):
  214. assert(self.logged_in())
  215. def compress_deposit_commands():
  216. if not self.transfers:
  217. return
  218. dummy, initial_balance = self.transfers[0]
  219. balance_before = initial_balance
  220. compressed_deposit = DepositCommand(Decimal('0'))
  221. others = list()
  222. for (command, dummy) in list(self.transfers):
  223. if isinstance(command, DepositCommand):
  224. compressed_deposit.add(command)
  225. else:
  226. others.append((command, balance_before))
  227. balance_before += command.difference()
  228. if compressed_deposit.difference() != 0:
  229. others.append((compressed_deposit, balance_before))
  230. self.transfers = others
  231. def process_buy_commands_combined():
  232. if not self.transfers:
  233. return
  234. # Compress BuyCommands, use a single bulkbuy
  235. dummy, initial_balance = self.transfers[0]
  236. balance_before = initial_balance
  237. buy_commands = list()
  238. non_buy_commands = list()
  239. total_buy_diff = 0
  240. for command, dummy in self.transfers:
  241. if isinstance(command, BuyCommand):
  242. buy_commands.append(command)
  243. else:
  244. balance_before += command.difference()
  245. non_buy_commands.append((command, balance_before))
  246. try:
  247. net.bulk_buy(buy_commands, self.login_name)
  248. except urllib2.HTTPError as e:
  249. myDisplay.display_screen("Server error",'Server Error: %s' % str(e))
  250. error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e))
  251. else:
  252. self.transfers = non_buy_commands
  253. def process_commands():
  254. for (command, balance_backup) in list(self.transfers):
  255. try:
  256. command.run(self.login_name)
  257. except urllib2.HTTPError as e:
  258. myDisplay.display_screen("Server error",'Server Error: %s' % str(e))
  259. error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e))
  260. break
  261. else:
  262. self.transfers.pop(0)
  263. def finish():
  264. if not self.transfers:
  265. # Show final balance for some time
  266. clear()
  267. self.dump()
  268. delay('Logout', 3)
  269. self.logout()
  270. compress_deposit_commands()
  271. process_buy_commands_combined()
  272. process_commands()
  273. finish()
  274. def find(self, barcode):
  275. try:
  276. return self.item_cache[barcode]
  277. except KeyError:
  278. item = net.get_item(barcode)
  279. self.item_cache[barcode] = item
  280. return item
  281. def buy(self, item):
  282. assert(self.logged_in())
  283. log_entry = (BuyCommand(item), self.balance)
  284. self.transfers.append(log_entry)
  285. self.balance = self.balance - item.price
  286. def deposit(self, amount):
  287. assert(self.logged_in())
  288. log_entry = (DepositCommand(amount), self.balance)
  289. self.transfers.append(log_entry)
  290. self.balance = self.balance + amount
  291. def to_go(self, scope):
  292. _PRODUCT_FIRST = 'FEHLER: Bitte zuerst den betreffenden Artikel scannen.'
  293. if not self.transfers:
  294. error_page(_PRODUCT_FIRST)
  295. return
  296. if scope == TO_GO_ALL:
  297. """
  298. Makes all BuyCommands with deposit > 0 include deposit
  299. ...and updates future balance accordingly.
  300. """
  301. dummy, initial_balance = self.transfers[0]
  302. balance_before = initial_balance
  303. for command, dummy in self.transfers:
  304. if isinstance(command, BuyCommand) \
  305. and not command.includes_deposit() \
  306. and command.deposit_value() > 0:
  307. command.include_deposit(True)
  308. balance_before += command.difference()
  309. self.balance = balance_before
  310. elif scope == TO_GO_NONE:
  311. """
  312. Makes all BuyCommands that include commodity
  313. not include deposit any more
  314. ...and updates future balance accordingly.
  315. """
  316. first_command, initial_balance = self.transfers[0]
  317. balance_before = initial_balance
  318. for command, dummy in self.transfers:
  319. if isinstance(command, BuyCommand) \
  320. and command.includes_commodity():
  321. command.include_deposit(False)
  322. balance_before += command.difference()
  323. self.balance = balance_before
  324. elif scope == TO_GO_PREV:
  325. """
  326. Makes the last BuyCommand include deposit
  327. ...and updates future balance accordingly.
  328. """
  329. prev, balance_backup = self.transfers[-1]
  330. if not isinstance(prev, BuyCommand):
  331. error_page(_PRODUCT_FIRST)
  332. return
  333. if prev.includes_deposit():
  334. myDisplay.cmd_clear()
  335. myDisplay.write('FEHLER: schon Pfand %20s' % prev.item_name()[:20])
  336. error_page('FEHLER: Pfand für Produkt "%s" bereits aktiviert' % prev.item_name())
  337. return
  338. if prev.deposit_value() <= 0:
  339. myDisplay.cmd_clear()
  340. myDisplay.write('FEHLER: Pfandfrei! %20s' % prev.item_name()[:20])
  341. error_page('FEHLER: Produkt "%s" hat kein Pfand' % prev.item_name())
  342. return
  343. before = prev.difference()
  344. prev.include_deposit(True)
  345. after = prev.difference()
  346. self.balance += (after - before)
  347. elif scope == TO_GO_ONLY:
  348. """
  349. Makes all BuyCommand that include commodity
  350. be deposit only
  351. ...and updates future balance accordingly.
  352. """
  353. dummy, initial_balance = self.transfers[0]
  354. balance_before = initial_balance
  355. for command, dummy in self.transfers:
  356. if isinstance(command, BuyCommand) \
  357. and command.deposit_value() > 0 \
  358. and command.includes_commodity():
  359. command.include_commodity(False)
  360. command.include_deposit(True)
  361. balance_before += command.difference()
  362. self.balance = balance_before
  363. def undo(self):
  364. assert(self.logged_in())
  365. if self.transfers:
  366. dummy, balance_backup = self.transfers[-1]
  367. self.transfers.pop()
  368. self.balance = balance_backup
  369. else:
  370. error_page('FEHLER: Nichts da, was ich rückgängig machen könnte.')
  371. def print_prompt():
  372. sys.stdout.write(">>> ")
  373. sys.stdout.flush()
  374. def handle(line, status):
  375. myDisplay.setIdlemessage("Mir ist langweilig!")
  376. if line == 'exit':
  377. clear()
  378. myDisplay.cmd_clear()
  379. myDisplay.terminate()
  380. sys.exit(0)
  381. if status.logged_in():
  382. if line in CODES:
  383. call = CODES[line]
  384. method = call[0]
  385. params = call[1:]
  386. getattr(status, method)(*params)
  387. else:
  388. try:
  389. item = status.find(line)
  390. except urllib2.HTTPError as e:
  391. if e.code == 404: # URL not found == item not found with REST
  392. myDisplay.display_screen("FEHLER","Code ist unbekannt: '%s'" % ( line[:23]))
  393. error_page('FEHLER: Aktion oder Ware "%s" nicht bekannt' % line)
  394. else:
  395. myDisplay.display_screen("Server error",'%20s' % str(e)[:20])
  396. error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e))
  397. else:
  398. status.buy(item)
  399. else:
  400. try:
  401. status.login(line)
  402. myDisplay.setIdlemessage(" Comitten nicht vergessen! ***")
  403. except urllib2.HTTPError as e:
  404. if e.code == 404: # URL not found == user unknown
  405. # Try same code as a product
  406. item = None
  407. try:
  408. item = status.find(line)
  409. except urllib2.HTTPError as e:
  410. pass
  411. if item is None:
  412. myDisplay.display_screen("FEHLER","Nutzer ist unbekannt: '%s' *** " % line)
  413. error_page('FEHLER: Produkt oder Nutzer "%s" nicht bekannt' % line,
  414. hint_message='Ist in der WebApp unter "Einstellungen" ' \
  415. 'für Ihren Account Plugin "BarcodePlugin" ' \
  416. 'als erlaubt markiert?')
  417. else:
  418. item_info_page(item)
  419. else:
  420. myDisplay.display_screen("FEHLER",'Server Error %20s' % str(e)[:20])
  421. error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e))
  422. except urllib2.URLError as e:
  423. error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e))
  424. def read_line(f, timeout, timeout_func):
  425. ready_to_read, _, _ = select.select([f], [], [], timeout)
  426. if ready_to_read:
  427. return f.readline().rstrip()
  428. else:
  429. timeout_func()
  430. return ''
  431. def main():
  432. colorama.init()
  433. status = Status()
  434. global scroll_line1,scroll_line2
  435. global myDisplay
  436. myDisplay = Display("/dev/ttyUSB0")
  437. myDisplay.start()
  438. myDisplay.cmd_reset()
  439. myDisplay.cmd_cursor_show(False)
  440. myDisplay.display_screen("Bitte Geduld","Initialisierung... ")
  441. while True:
  442. clear()
  443. status.dump()
  444. print_prompt()
  445. line = read_line(sys.stdin, timeout=3*60.0, timeout_func=status.logout)
  446. if line:
  447. myDisplay.cmd_clear()
  448. myDisplay.write('RFID/Barcode:\n%20s' % line[:20])
  449. handle(line, status)
  450. if __name__ == '__main__':
  451. try:
  452. main()
  453. except KeyboardInterrupt:
  454. myDisplay.terminate()
  455. myDisplay.cmd_reset()
  456. myDisplay.cmd_cursor_show(False)
  457. pass