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.
k4ever/client-barcode/freitagskasse.py

559 rivejä
15 KiB

#! /usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2011 Sebastian Pipping <sebastian@pipping.org>
# Licensed under GPL v3 or later
from __future__ import print_function
import freitagslib.network as net
from freitagslib.commands import BuyCommand, DepositCommand
from freitagslib.encoding import asciify
from freitagslib.settings import settings, Settings
import colorama
from colorama import Fore, Style
import sys
from decimal import Decimal
import os
import time
import urllib2
import select
from display import Display
from thread import start_new_thread, allocate_lock
import time
myDisplay=None
COLOR_HINT = Fore.YELLOW + Style.BRIGHT
COLOR_ERROR = Fore.RED
COLOR_DEPOSIT = Fore.GREEN + Style.BRIGHT
COLOR_TO_GO = Fore.MAGENTA + Style.BRIGHT
COLOR_WITHDRAW = Fore.RED + Style.BRIGHT
COLOR_SOME = Fore.WHITE + Style.BRIGHT
COLOR_MUCH = Fore.YELLOW + Style.BRIGHT
COLOR_RESET = Style.RESET_ALL
display_fifo = None
scroll_line1 = None
scroll_line2 = None
offset_line1 = 0
offset_line2 = 0
brightness = 5
screensaver = 0
SCREENSAVER_DIM = 2* 20
SCREENSAVER_TIMEOUT = 2* 120
SCREENSAVER_OFF = 2* 10*60
lock = allocate_lock()
def clear():
os.system('clear')
TO_GO_ONLY = 2
TO_GO_ALL = 1
TO_GO_NONE = 0
TO_GO_PREV = -1
CODES = {
'UNDO':('undo',),
'COMMIT':('commit',),
'TO GO ALL':('to_go', TO_GO_ALL),
'TO GO NONE':('to_go', TO_GO_NONE),
'TO GO PREV':('to_go', TO_GO_PREV),
'TO GO ONLY':('to_go', TO_GO_ONLY),
'TO GO ONLZ':('to_go', TO_GO_ONLY), # Workaround for German keyboard layouts
'DEPOSIT 0.01':('deposit', Decimal('0.01')),
'DEPOSIT 0.05':('deposit', Decimal('0.05')),
'DEPOSIT 0.10':('deposit', Decimal('0.10')),
'DEPOSIT 0.50':('deposit', Decimal('0.50')),
'DEPOSIT 1.00':('deposit', Decimal('1.00')),
'DEPOSIT 5.00':('deposit', Decimal('5.00')),
'DEPOSIT 10.00':('deposit', Decimal('10.00')),
'DEPOSIT 50.00':('deposit', Decimal('50.00')),
}
def delay(what, seconds):
for i in xrange(seconds, 0, -1):
if i < seconds:
sys.stdout.write('\r')
sys.stdout.write('%s in %d Sekunden... ' % (what, i))
sys.stdout.flush()
(read_list, _, _) = select.select([sys.stdin], [], [], 1)
if len(read_list) > 0:
# input on stdin, quit delay
return
def warn_balance():
print('Kontostand im Minus, bitte Geld aufladen.')
def error_page(error_message, hint_message=None):
clear()
print(COLOR_ERROR + error_message + COLOR_RESET)
print()
delay_seconds = 3
if hint_message is not None:
print(COLOR_HINT + hint_message + COLOR_RESET)
print()
delay_seconds += 3
delay('Weiter', delay_seconds)
def item_info_page(item):
indent = 4 * ' '
clear()
print('Diese Ware heißt')
print()
print(indent + COLOR_SOME + item.name + COLOR_RESET)
print()
print('und kostet')
print()
if item.deposit > 0:
myDisplay.display_screen("PREISINFO","%s: %4.2f Euro (%4.2f Euro Pfand)" % (asciify(item.name), item.price, item.deposit))
print(indent + '%s%4.2f Euro%s + %4.2f Euro Pfand = %s%4.2f Euro%s .' \
% (COLOR_SOME, item.price, COLOR_RESET, item.deposit,
COLOR_MUCH, item.price + item.deposit, COLOR_RESET))
else:
myDisplay.display_screen("PREISINFO","%s: %4.2f Euro" % (asciify(item.name), item.price))
print(indent + '%s%4.2f Euro%s .' \
% (COLOR_MUCH, item.price, COLOR_RESET))
print()
print()
print(COLOR_MUCH + 'Zum Kaufen bitte einloggen.' + COLOR_RESET)
print()
delay('Weiter', 6)
class Status:
def __init__(self):
self._reset()
self.item_cache = dict()
def _reset(self):
self.auth_blob = None
self.login_name = None
self.balance = None
self.transfers = None
def dump(self):
def sign(amount, plus='+'):
return '-' if amount < 0 else plus
def color(amount):
return COLOR_WITHDRAW if amount < 0 else COLOR_DEPOSIT
def show_total(balance, plus=' '):
print('%3s %-40s %s%c %6.2f Euro%s' \
% ('', '', color(balance), sign(balance, plus),
abs(balance), COLOR_RESET))
def shorten(text, length):
if len(text) <= length:
return text
else:
return text[:length - 3] + '...'
def show_item(position, diff, label, color):
print('%2d) %-40s %s%c %6.2f Euro%s' \
% (position, shorten(label, 40), color, sign(diff),
abs(diff), COLOR_RESET))
def show_bar():
print('%3s %-40s %13s' % ('', '', '============='))
if self.logged_in():
print('Eingeloggt als: %s%s%s' % (COLOR_SOME, self.login_name, COLOR_RESET))
print()
myDisplay.cmd_clear()
myDisplay.write('Hallo %-14s' % (self.login_name[:13]+"!") )
if self.transfers:
initial_command, initial_balance = self.transfers[0]
print('Geplante Änderungen:')
show_total(initial_balance)
i = 1
for command, dummy in self.transfers:
if isinstance(command, BuyCommand):
if (command.includes_commodity()):
show_item(i, -command.commodity_value(), command.commodity_label(), COLOR_WITHDRAW)
i += 1
if (command.includes_deposit()):
show_item(i, -command.deposit_value(), command.deposit_label(), COLOR_TO_GO)
i += 1
else:
show_item(i, command.difference(), command.label(), COLOR_DEPOSIT)
i += 1
show_bar()
if isinstance(command, BuyCommand):
mycmd = 0;
if (command.includes_commodity()):
mycmd+=1;
if (command.includes_deposit()):
mycmd+=2;
if (mycmd==1):
mylabel=command.item_name()
if (mycmd==2):
mylabel="Pfand "+command.commodity_label()[:9]
if (mycmd==3):
mylabel=("%-13s" % (command.commodity_label()[:13]))+"+P"
myDisplay.cmd_home()
myDisplay.write('%-15s %4.2f' % (asciify(mylabel)[:15],abs(command.difference())));
myDisplay.cmd_home()
myDisplay.write('\nSUMME: {%02i} %8.2f' % ((i-1),initial_balance - self.balance));
else:
myDisplay.cmd_home()
myDisplay.write('%-15s %4.2f' % (command.label()[:15],abs(command.difference())));
myDisplay.cmd_home()
myDisplay.write('\nSUMME: {%02i} %8.2f' % ((i-1),initial_balance - self.balance));
if len(self.transfers) > 1:
show_total(self.balance - initial_balance, plus='+')
show_bar()
show_total(self.balance)
if self.balance < 0:
warn_balance()
myDisplay.cmd_home()
myDisplay.write('\nKonto: %5.2f!' % (self.balance) )
print()
print(COLOR_MUCH + 'Committen nicht vergessen.' + COLOR_RESET)
else:
print('Kontostand beträgt: %s%.2f Euro%s' % (COLOR_MUCH, self.balance, COLOR_RESET))
myDisplay.display_screen("KONTOSTAND","%s: %.2f Euro" % (self.login_name,self.balance))
if self.balance < 0:
print()
warn_balance()
else:
print(COLOR_MUCH + 'Bitte einloggen.' + COLOR_RESET)
print()
print('Scanne dazu deine ID-Karte mit dem Barcode-Leser.')
myDisplay.display_screen("LOGIN","Bitte scanne Deinen Login-Token! *** ")
print()
def logged_in(self):
return self.auth_blob is not None
def login(self, auth_blob):
assert(not self.logged_in())
user_name = net.get_user_name_from_auth_blob(auth_blob)
balance = net.get_balance(user_name)
self.auth_blob = auth_blob
self.login_name = user_name
self.balance = balance
self.transfers = list()
def logout(self):
# Must not fail if not logged in
self._reset()
def commit(self):
assert(self.logged_in())
def compress_deposit_commands():
if not self.transfers:
return
dummy, initial_balance = self.transfers[0]
balance_before = initial_balance
compressed_deposit = DepositCommand(Decimal('0'))
others = list()
for (command, dummy) in list(self.transfers):
if isinstance(command, DepositCommand):
compressed_deposit.add(command)
else:
others.append((command, balance_before))
balance_before += command.difference()
if compressed_deposit.difference() != 0:
others.append((compressed_deposit, balance_before))
self.transfers = others
def process_buy_commands_combined():
if not self.transfers:
return
# Compress BuyCommands, use a single bulkbuy
dummy, initial_balance = self.transfers[0]
balance_before = initial_balance
buy_commands = list()
non_buy_commands = list()
total_buy_diff = 0
for command, dummy in self.transfers:
if isinstance(command, BuyCommand):
buy_commands.append(command)
else:
balance_before += command.difference()
non_buy_commands.append((command, balance_before))
try:
net.bulk_buy(buy_commands, self.login_name)
except urllib2.HTTPError as e:
myDisplay.display_screen("Server error",'Server Error: %s' % str(e))
error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e))
else:
self.transfers = non_buy_commands
def process_commands():
for (command, balance_backup) in list(self.transfers):
try:
command.run(self.login_name)
except urllib2.HTTPError as e:
myDisplay.display_screen("Server error",'Server Error: %s' % str(e))
error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e))
break
else:
self.transfers.pop(0)
def finish():
if not self.transfers:
# Show final balance for some time
clear()
self.dump()
delay('Logout', 3)
self.logout()
compress_deposit_commands()
process_buy_commands_combined()
process_commands()
finish()
def find(self, barcode):
try:
return self.item_cache[barcode]
except KeyError:
item = net.get_item(barcode)
self.item_cache[barcode] = item
return item
def buy(self, item):
assert(self.logged_in())
log_entry = (BuyCommand(item), self.balance)
self.transfers.append(log_entry)
self.balance = self.balance - item.price
def deposit(self, amount):
assert(self.logged_in())
log_entry = (DepositCommand(amount), self.balance)
self.transfers.append(log_entry)
self.balance = self.balance + amount
def to_go(self, scope):
_PRODUCT_FIRST = 'FEHLER: Bitte zuerst den betreffenden Artikel scannen.'
if not self.transfers:
error_page(_PRODUCT_FIRST)
return
if scope == TO_GO_ALL:
"""
Makes all BuyCommands with deposit > 0 include deposit
...and updates future balance accordingly.
"""
dummy, initial_balance = self.transfers[0]
balance_before = initial_balance
for command, dummy in self.transfers:
if isinstance(command, BuyCommand) \
and not command.includes_deposit() \
and command.deposit_value() > 0:
command.include_deposit(True)
balance_before += command.difference()
self.balance = balance_before
elif scope == TO_GO_NONE:
"""
Makes all BuyCommands that include commodity
not include deposit any more
...and updates future balance accordingly.
"""
first_command, initial_balance = self.transfers[0]
balance_before = initial_balance
for command, dummy in self.transfers:
if isinstance(command, BuyCommand) \
and command.includes_commodity():
command.include_deposit(False)
balance_before += command.difference()
self.balance = balance_before
elif scope == TO_GO_PREV:
"""
Makes the last BuyCommand include deposit
...and updates future balance accordingly.
"""
prev, balance_backup = self.transfers[-1]
if not isinstance(prev, BuyCommand):
error_page(_PRODUCT_FIRST)
return
if prev.includes_deposit():
myDisplay.cmd_clear()
myDisplay.write('FEHLER: schon Pfand %20s' % prev.item_name()[:20])
error_page('FEHLER: Pfand für Produkt "%s" bereits aktiviert' % prev.item_name())
return
if prev.deposit_value() <= 0:
myDisplay.cmd_clear()
myDisplay.write('FEHLER: Pfandfrei! %20s' % prev.item_name()[:20])
error_page('FEHLER: Produkt "%s" hat kein Pfand' % prev.item_name())
return
before = prev.difference()
prev.include_deposit(True)
after = prev.difference()
self.balance += (after - before)
elif scope == TO_GO_ONLY:
"""
Makes all BuyCommand that include commodity
be deposit only
...and updates future balance accordingly.
"""
dummy, initial_balance = self.transfers[0]
balance_before = initial_balance
for command, dummy in self.transfers:
if isinstance(command, BuyCommand) \
and command.deposit_value() > 0 \
and command.includes_commodity():
command.include_commodity(False)
command.include_deposit(True)
balance_before += command.difference()
self.balance = balance_before
def undo(self):
assert(self.logged_in())
if self.transfers:
dummy, balance_backup = self.transfers[-1]
self.transfers.pop()
self.balance = balance_backup
else:
error_page('FEHLER: Nichts da, was ich rückgängig machen könnte.')
def print_prompt():
sys.stdout.write(">>> ")
sys.stdout.flush()
def handle(line, status):
myDisplay.setIdlemessage("Mir ist langweilig!")
if line == 'exit':
clear()
myDisplay.cmd_clear()
myDisplay.terminate()
sys.exit(0)
if status.logged_in():
if line in CODES:
call = CODES[line]
method = call[0]
params = call[1:]
getattr(status, method)(*params)
else:
try:
item = status.find(line)
except urllib2.HTTPError as e:
if e.code == 404: # URL not found == item not found with REST
myDisplay.display_screen("FEHLER","Code ist unbekannt: '%s'" % ( line[:23]))
error_page('FEHLER: Aktion oder Ware "%s" nicht bekannt' % line)
else:
myDisplay.display_screen("Server error",'%20s' % str(e)[:20])
error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e))
else:
status.buy(item)
else:
try:
status.login(line)
myDisplay.setIdlemessage(" Comitten nicht vergessen! ***")
except urllib2.HTTPError as e:
if e.code == 404: # URL not found == user unknown
# Try same code as a product
item = None
try:
item = status.find(line)
except urllib2.HTTPError as e:
pass
if item is None:
myDisplay.display_screen("FEHLER","Nutzer ist unbekannt: '%s' *** " % line)
error_page('FEHLER: Produkt oder Nutzer "%s" nicht bekannt' % line,
hint_message='Ist in der WebApp unter "Einstellungen" ' \
'für Ihren Account Plugin "BarcodePlugin" ' \
'als erlaubt markiert?')
else:
item_info_page(item)
else:
myDisplay.display_screen("FEHLER",'Server Error %20s' % str(e)[:20])
error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e))
except urllib2.URLError as e:
error_page('FEHLER bei Kommunikation mit Server "%s"' % str(e))
def read_line(f, timeout, timeout_func):
ready_to_read, _, _ = select.select([f], [], [], timeout)
if ready_to_read:
return f.readline().rstrip()
else:
timeout_func()
return ''
def main():
colorama.init()
if not settings.load("freitagskasse.conf"):
sys.exit(1)
status = Status()
global scroll_line1,scroll_line2
global myDisplay
myDisplay = Display("/dev/ttyUSB0")
myDisplay.start()
myDisplay.cmd_reset()
myDisplay.cmd_cursor_show(False)
myDisplay.display_screen("Bitte Geduld","Initialisierung... ")
while True:
clear()
status.dump()
print_prompt()
line = read_line(sys.stdin, timeout=3*60.0, timeout_func=status.logout)
if line:
myDisplay.cmd_clear()
myDisplay.write('RFID/Barcode:\n%20s' % line[:20])
handle(line, status)
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
myDisplay.terminate()
myDisplay.cmd_reset()
myDisplay.cmd_cursor_show(False)
pass