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

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