This commit is contained in:
parent
4d254c612f
commit
12ad58961c
1418
text_based.py
1418
text_based.py
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,8 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
exit_commands = ['exit', 'abort', 'quit', 'bye', 'eat flaming death', 'q']
|
||||||
|
help_commands = ['help', '?']
|
||||||
|
context_commands = ['what', '??']
|
||||||
|
local_help_commands = ['help!', '???']
|
||||||
|
faq_commands = ['faq']
|
||||||
|
restart_commands = ['restart']
|
|
@ -0,0 +1,124 @@
|
||||||
|
from math import ceil
|
||||||
|
|
||||||
|
import sqlalchemy
|
||||||
|
from db import Product, User, Transaction
|
||||||
|
from text_interface.helpermenus import Menu
|
||||||
|
|
||||||
|
|
||||||
|
class AddStockMenu(Menu):
|
||||||
|
def __init__(self):
|
||||||
|
Menu.__init__(self, 'Add stock and adjust credit', uses_db=True)
|
||||||
|
self.help_text = '''
|
||||||
|
Enter what you have bought for PVVVV here, along with your user name and how
|
||||||
|
much money you're due in credits for the purchase when prompted.\n'''
|
||||||
|
self.user = None
|
||||||
|
|
||||||
|
def _execute(self):
|
||||||
|
questions = {
|
||||||
|
(False, False): 'Enter user id or a string of the form "<number> <product>"',
|
||||||
|
(False, True): 'Enter user id or more strings of the form "<number> <product>"',
|
||||||
|
(True, False): 'Enter a string of the form "<number> <product>"',
|
||||||
|
(True, True): 'Enter more strings of the form "<number> <product>", or an empty line to confirm'
|
||||||
|
}
|
||||||
|
|
||||||
|
self.user = None
|
||||||
|
self.products = {}
|
||||||
|
self.price = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
self.print_info()
|
||||||
|
self.printc(questions[bool(self.user), bool(len(self.products))])
|
||||||
|
thing_price = 0
|
||||||
|
|
||||||
|
# Read in a 'thing' (product or user):
|
||||||
|
line = self.input_multiple(add_nonexisting=('user', 'product'), empty_input_permitted=True,
|
||||||
|
find_hidden_products=False)
|
||||||
|
|
||||||
|
if line:
|
||||||
|
(thing, amount) = line
|
||||||
|
|
||||||
|
if isinstance(thing, Product):
|
||||||
|
self.printc("%d of %s registered" % (amount, thing.name))
|
||||||
|
thing_price = self.input_int('What did you pay a piece? ', (1, 100000),
|
||||||
|
default=thing.price) * amount
|
||||||
|
self.price += thing_price
|
||||||
|
|
||||||
|
# once we get something in the
|
||||||
|
# purchase, we want to protect the
|
||||||
|
# user from accidentally killing it
|
||||||
|
self.exit_confirm_msg = 'Abort transaction?'
|
||||||
|
else:
|
||||||
|
# thing = None
|
||||||
|
|
||||||
|
if not self.complete_input():
|
||||||
|
if self.confirm('Not enough information entered. Abort transaction?', default=True):
|
||||||
|
return False
|
||||||
|
continue
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
# Add the thing to the pending adjustments:
|
||||||
|
self.add_thing_to_pending(thing, amount, thing_price)
|
||||||
|
|
||||||
|
if self.confirm('Do you want to change the credited amount?', default=False):
|
||||||
|
self.price = self.input_int('Total amount> ', (1, 100000), default=self.price)
|
||||||
|
|
||||||
|
self.perform_transaction()
|
||||||
|
|
||||||
|
def complete_input(self):
|
||||||
|
return bool(self.user) and len(self.products) and self.price
|
||||||
|
|
||||||
|
def print_info(self):
|
||||||
|
print(6 + Product.name_length) * '-'
|
||||||
|
if self.price:
|
||||||
|
print("Amount to be credited: %" + str(Product.name_length - 17) + "i") % self.price
|
||||||
|
if self.user:
|
||||||
|
print("User to credit: %" + str(Product.name_length - 10) + "s") % self.user.name
|
||||||
|
print('\n%-' + str(Product.name_length - 1) + 's Amount') % "Product"
|
||||||
|
print(6 + Product.name_length) * '-'
|
||||||
|
if len(self.products):
|
||||||
|
# print "Products added:"
|
||||||
|
# print (6+Product.name_length)*'-'
|
||||||
|
for product in self.products.keys():
|
||||||
|
print('%' + str(-Product.name_length) + 's %5i') % (product.name, self.products[product][0])
|
||||||
|
print(6 + Product.name_length) * '-'
|
||||||
|
|
||||||
|
def add_thing_to_pending(self, thing, amount, price):
|
||||||
|
if isinstance(thing, User):
|
||||||
|
if self.user:
|
||||||
|
print "Only one user may be credited for a purchase, transfer credit manually afterwards"
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self.user = thing
|
||||||
|
elif thing in self.products.keys():
|
||||||
|
print 'Already added this product, adding amounts'
|
||||||
|
self.products[thing][0] += amount
|
||||||
|
self.products[thing][1] += price
|
||||||
|
else:
|
||||||
|
self.products[thing] = [amount, price]
|
||||||
|
|
||||||
|
def perform_transaction(self):
|
||||||
|
# self.user.credit += self.price
|
||||||
|
description = self.input_str('Log message> ', length_range=(0, 50))
|
||||||
|
if description == '':
|
||||||
|
description = 'Purchased products for PVVVV, adjusted credit ' + str(self.price)
|
||||||
|
transaction = Transaction(self.user, -self.price, description)
|
||||||
|
transaction.perform_transaction()
|
||||||
|
self.session.add(transaction)
|
||||||
|
for product in self.products:
|
||||||
|
value = max(product.stock, 0) * product.price + self.products[product][1]
|
||||||
|
old_price = product.price
|
||||||
|
old_hidden = product.hidden
|
||||||
|
product.price = int(ceil(float(value) / (max(product.stock, 0) + self.products[product][0])))
|
||||||
|
product.stock += self.products[product][0]
|
||||||
|
product.hidden = False
|
||||||
|
print "New stock for %s: %d" % (product.name, product.stock), \
|
||||||
|
("- New price: " + str(product.price) if old_price != product.price else ""), \
|
||||||
|
("- Removed hidden status" if old_hidden != product.hidden else "")
|
||||||
|
try:
|
||||||
|
self.session.commit()
|
||||||
|
print "Success! Transaction performed:"
|
||||||
|
# self.print_info()
|
||||||
|
print "User %s's credit is now %i" % (self.user.name, self.user.credit)
|
||||||
|
except sqlalchemy.exc.SQLAlchemyError, e:
|
||||||
|
print 'Could not perform transaction: %s' % e
|
|
@ -0,0 +1,201 @@
|
||||||
|
import conf
|
||||||
|
import sqlalchemy
|
||||||
|
from db import User, Purchase, PurchaseEntry, Transaction, Product
|
||||||
|
from text_interface.helpermenus import Menu
|
||||||
|
|
||||||
|
|
||||||
|
class BuyMenu(Menu):
|
||||||
|
def __init__(self, session=None):
|
||||||
|
Menu.__init__(self, 'Buy', uses_db=True)
|
||||||
|
if session:
|
||||||
|
self.session = session
|
||||||
|
self.superfast_mode = False
|
||||||
|
self.help_text = '''
|
||||||
|
Each purchase may contain one or more products and one or more buyers.
|
||||||
|
|
||||||
|
Enter products (by name or bar code) and buyers (by name or bar code)
|
||||||
|
in any order. The information gathered so far is displayed after each
|
||||||
|
addition, and you can type 'what' at any time to redisplay it.
|
||||||
|
|
||||||
|
When finished, write an empty line to confirm the purchase.\n'''
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def credit_check(user):
|
||||||
|
"""
|
||||||
|
|
||||||
|
:param user:
|
||||||
|
:type user: User
|
||||||
|
:rtype: boolean
|
||||||
|
"""
|
||||||
|
assert isinstance(user, User)
|
||||||
|
|
||||||
|
return user.credit > conf.low_credit_warning_limit
|
||||||
|
|
||||||
|
def low_credit_warning(self, user, timeout=False):
|
||||||
|
assert isinstance(user, User)
|
||||||
|
|
||||||
|
print "***********************************************************************"
|
||||||
|
print "***********************************************************************"
|
||||||
|
print ""
|
||||||
|
print "$$\ $$\ $$$$$$\ $$$$$$$\ $$\ $$\ $$$$$$\ $$\ $$\ $$$$$$\\"
|
||||||
|
print "$$ | $\ $$ |$$ __$$\ $$ __$$\ $$$\ $$ |\_$$ _|$$$\ $$ |$$ __$$\\"
|
||||||
|
print "$$ |$$$\ $$ |$$ / $$ |$$ | $$ |$$$$\ $$ | $$ | $$$$\ $$ |$$ / \__|"
|
||||||
|
print "$$ $$ $$\$$ |$$$$$$$$ |$$$$$$$ |$$ $$\$$ | $$ | $$ $$\$$ |$$ |$$$$\\"
|
||||||
|
print "$$$$ _$$$$ |$$ __$$ |$$ __$$< $$ \$$$$ | $$ | $$ \$$$$ |$$ |\_$$ |"
|
||||||
|
print "$$$ / \$$$ |$$ | $$ |$$ | $$ |$$ |\$$$ | $$ | $$ |\$$$ |$$ | $$ |"
|
||||||
|
print "$$ / \$$ |$$ | $$ |$$ | $$ |$$ | \$$ |$$$$$$\ $$ | \$$ |\$$$$$$ |"
|
||||||
|
print "\__/ \__|\__| \__|\__| \__|\__| \__|\______|\__| \__| \______/"
|
||||||
|
print ""
|
||||||
|
print "***********************************************************************"
|
||||||
|
print "***********************************************************************"
|
||||||
|
print ""
|
||||||
|
print "USER %s HAS LOWER CREDIT THAN %d." % (user.name, conf.low_credit_warning_limit)
|
||||||
|
print "THIS PURCHASE WILL CHARGE YOUR CREDIT TWICE AS MUCH."
|
||||||
|
print "CONSIDER PUTTING MONEY IN THE BOX TO AVOID THIS."
|
||||||
|
print ""
|
||||||
|
print "Do you want to continue with this purchase?"
|
||||||
|
|
||||||
|
if timeout:
|
||||||
|
print"THIS PURCHASE WILL AUTOMATICALLY BE PERFORMED IN 3 MINUTES!"
|
||||||
|
return self.confirm(prompt=">", default=True, timeout=180)
|
||||||
|
else:
|
||||||
|
return self.confirm(prompt=">", default=True)
|
||||||
|
|
||||||
|
def add_thing_to_purchase(self, thing, amount=1):
|
||||||
|
if isinstance(thing, User):
|
||||||
|
if thing.is_anonymous():
|
||||||
|
print '--------------------------------------------'
|
||||||
|
print'You are now purchasing as the user anonym.'
|
||||||
|
print'You have to put money in the anonym-jar.'
|
||||||
|
print '--------------------------------------------'
|
||||||
|
|
||||||
|
if not self.credit_check(thing):
|
||||||
|
if self.low_credit_warning(user=thing, timeout=self.superfast_mode):
|
||||||
|
Transaction(thing, purchase=self.purchase, penalty=2)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
Transaction(thing, purchase=self.purchase)
|
||||||
|
elif isinstance(thing, Product):
|
||||||
|
PurchaseEntry(self.purchase, thing, amount)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _execute(self, initial_contents=None):
|
||||||
|
self.print_header()
|
||||||
|
self.purchase = Purchase()
|
||||||
|
self.exit_confirm_msg = None
|
||||||
|
self.superfast_mode = False
|
||||||
|
|
||||||
|
if initial_contents is None:
|
||||||
|
initial_contents = []
|
||||||
|
|
||||||
|
for thing, num in initial_contents:
|
||||||
|
self.add_thing_to_purchase(thing, num)
|
||||||
|
|
||||||
|
def is_product(candidate):
|
||||||
|
return isinstance(candidate, Product)
|
||||||
|
|
||||||
|
if len(initial_contents) > 0 and all(map(is_product, initial_contents)):
|
||||||
|
self.superfast_mode = True
|
||||||
|
print '***********************************************'
|
||||||
|
print '****** Buy menu is in SUPERFASTmode[tm]! ******'
|
||||||
|
print '*** The purchase will be stored immediately ***'
|
||||||
|
print '*** when you enter a user. ***'
|
||||||
|
print '***********************************************'
|
||||||
|
|
||||||
|
while True:
|
||||||
|
self.print_purchase()
|
||||||
|
self.printc({(False, False): 'Enter user or product identification',
|
||||||
|
(False, True): 'Enter user identification or more products',
|
||||||
|
(True, False): 'Enter product identification or more users',
|
||||||
|
(True, True): 'Enter more products or users, or an empty line to confirm'
|
||||||
|
}[(len(self.purchase.transactions) > 0,
|
||||||
|
len(self.purchase.entries) > 0)])
|
||||||
|
|
||||||
|
# Read in a 'thing' (product or user):
|
||||||
|
line = self.input_multiple(add_nonexisting=('user', 'product'), empty_input_permitted=True,
|
||||||
|
find_hidden_products=False)
|
||||||
|
|
||||||
|
thing, num = line
|
||||||
|
|
||||||
|
# Possibly exit from the menu:
|
||||||
|
if thing is None:
|
||||||
|
if not self.complete_input():
|
||||||
|
if self.confirm('Not enough information entered. Abort purchase?',
|
||||||
|
default=True):
|
||||||
|
return False
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# once we get something in the
|
||||||
|
# purchase, we want to protect the
|
||||||
|
# user from accidentally killing it
|
||||||
|
self.exit_confirm_msg = 'Abort purchase?'
|
||||||
|
|
||||||
|
# Add the thing to our purchase object:
|
||||||
|
if not self.add_thing_to_purchase(thing, amount=num):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# In super-fast mode, we complete the purchase once we get a user:
|
||||||
|
if self.superfast_mode and isinstance(thing, User):
|
||||||
|
break
|
||||||
|
|
||||||
|
self.purchase.perform_purchase()
|
||||||
|
self.session.add(self.purchase)
|
||||||
|
try:
|
||||||
|
self.session.commit()
|
||||||
|
except sqlalchemy.exc.SQLAlchemyError, e:
|
||||||
|
print 'Could not store purchase: %s' % e
|
||||||
|
else:
|
||||||
|
print 'Purchase stored.'
|
||||||
|
self.print_purchase()
|
||||||
|
for t in self.purchase.transactions:
|
||||||
|
if not t.user.is_anonymous():
|
||||||
|
print 'User %s\'s credit is now %d kr' % (t.user.name, t.user.credit)
|
||||||
|
if t.user.credit < conf.low_credit_warning_limit:
|
||||||
|
print 'USER %s HAS LOWER CREDIT THAN %d, AND SHOULD CONSIDER PUTTING SOME MONEY IN THE BOX.' \
|
||||||
|
% (t.user.name, conf.low_credit_warning_limit)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def complete_input(self):
|
||||||
|
return self.purchase.is_complete()
|
||||||
|
|
||||||
|
def format_purchase(self):
|
||||||
|
self.purchase.set_price()
|
||||||
|
transactions = self.purchase.transactions
|
||||||
|
entries = self.purchase.entries
|
||||||
|
if len(transactions) == 0 and len(entries) == 0:
|
||||||
|
return None
|
||||||
|
string = 'Purchase:'
|
||||||
|
string += '\n buyers: '
|
||||||
|
if len(transactions) == 0:
|
||||||
|
string += '(empty)'
|
||||||
|
else:
|
||||||
|
string += ', '.join(
|
||||||
|
map(lambda t: t.user.name + ("*" if t.user.credit < conf.user_recent_transaction_limit else ""),
|
||||||
|
transactions))
|
||||||
|
string += '\n products: '
|
||||||
|
if len(entries) == 0:
|
||||||
|
string += '(empty)'
|
||||||
|
else:
|
||||||
|
string += "\n "
|
||||||
|
string += '\n '.join(map(lambda e: '%dx %s (%d kr)' % (e.amount, e.product.name, e.product.price),
|
||||||
|
entries))
|
||||||
|
if len(transactions) > 1:
|
||||||
|
string += '\n price per person: %d kr' % self.purchase.price_per_transaction()
|
||||||
|
if any(t.penalty > 1 for t in transactions):
|
||||||
|
string += ' *(%d kr)' % (self.purchase.price_per_transaction() * 2)
|
||||||
|
|
||||||
|
string += '\n total price: %d kr' % self.purchase.price
|
||||||
|
|
||||||
|
if any(t.penalty > 1 for t in transactions):
|
||||||
|
string += '\n *total with penalty: %d kr' % sum(
|
||||||
|
self.purchase.price_per_transaction() * t.penalty for t in transactions)
|
||||||
|
|
||||||
|
return string
|
||||||
|
|
||||||
|
def print_purchase(self):
|
||||||
|
info = self.format_purchase()
|
||||||
|
if info is not None:
|
||||||
|
self.set_context(info)
|
|
@ -0,0 +1,187 @@
|
||||||
|
import sqlalchemy
|
||||||
|
from db import User, Product
|
||||||
|
from text_interface.helpermenus import Menu, Selector
|
||||||
|
|
||||||
|
__all__ = ["AddUserMenu", "AddProductMenu", "EditProductMenu", "AdjustStockMenu", "CleanupStockMenu", "EditUserMenu"]
|
||||||
|
|
||||||
|
|
||||||
|
class AddUserMenu(Menu):
|
||||||
|
def __init__(self):
|
||||||
|
Menu.__init__(self, 'Add user', uses_db=True)
|
||||||
|
|
||||||
|
def _execute(self):
|
||||||
|
self.print_header()
|
||||||
|
username = self.input_str('Username (should be same as PVV username)> ', User.name_re, (1, 10))
|
||||||
|
cardnum = self.input_str('Card number (optional)> ', User.card_re, (0, 10))
|
||||||
|
cardnum = cardnum.lower()
|
||||||
|
rfid = self.input_str('RFID (optional)> ', User.rfid_re, (0, 10))
|
||||||
|
user = User(username, cardnum, rfid)
|
||||||
|
self.session.add(user)
|
||||||
|
try:
|
||||||
|
self.session.commit()
|
||||||
|
print 'User %s stored' % username
|
||||||
|
except sqlalchemy.exc.IntegrityError, e:
|
||||||
|
print 'Could not store user %s: %s' % (username, e)
|
||||||
|
self.pause()
|
||||||
|
|
||||||
|
|
||||||
|
class EditUserMenu(Menu):
|
||||||
|
def __init__(self):
|
||||||
|
Menu.__init__(self, 'Edit user', uses_db=True)
|
||||||
|
self.help_text = '''
|
||||||
|
The only editable part of a user is its card number and rfid.
|
||||||
|
|
||||||
|
First select an existing user, then enter a new card number for that
|
||||||
|
user, then rfid (write an empty line to remove the card number or rfid).
|
||||||
|
'''
|
||||||
|
|
||||||
|
def _execute(self):
|
||||||
|
self.print_header()
|
||||||
|
user = self.input_user('User> ')
|
||||||
|
self.printc('Editing user %s' % user.name)
|
||||||
|
card_str = '"%s"' % user.card
|
||||||
|
if user.card is None:
|
||||||
|
card_str = 'empty'
|
||||||
|
user.card = self.input_str('Card number (currently %s)> ' % card_str,
|
||||||
|
User.card_re, (0, 10),
|
||||||
|
empty_string_is_none=True)
|
||||||
|
if user.card:
|
||||||
|
user.card = user.card.lower()
|
||||||
|
|
||||||
|
rfid_str = '"%s"' % user.rfid
|
||||||
|
if user.rfid is None:
|
||||||
|
rfid_str = 'empty'
|
||||||
|
user.rfid = self.input_str('RFID (currently %s)> ' % rfid_str,
|
||||||
|
User.rfid_re, (0, 10),
|
||||||
|
empty_string_is_none=True)
|
||||||
|
try:
|
||||||
|
self.session.commit()
|
||||||
|
print 'User %s stored' % user.name
|
||||||
|
except sqlalchemy.exc.SQLAlchemyError, e:
|
||||||
|
print 'Could not store user %s: %s' % (user.name, e)
|
||||||
|
self.pause()
|
||||||
|
|
||||||
|
|
||||||
|
class AddProductMenu(Menu):
|
||||||
|
def __init__(self):
|
||||||
|
Menu.__init__(self, 'Add product', uses_db=True)
|
||||||
|
|
||||||
|
def _execute(self):
|
||||||
|
self.print_header()
|
||||||
|
bar_code = self.input_str('Bar code> ', Product.bar_code_re, (8, 13))
|
||||||
|
name = self.input_str('Name> ', Product.name_re, (1, Product.name_length))
|
||||||
|
price = self.input_int('Price> ', (1, 100000))
|
||||||
|
product = Product(bar_code, name, price)
|
||||||
|
self.session.add(product)
|
||||||
|
try:
|
||||||
|
self.session.commit()
|
||||||
|
print 'Product %s stored' % name
|
||||||
|
except sqlalchemy.exc.SQLAlchemyError, e:
|
||||||
|
print 'Could not store product %s: %s' % (name, e)
|
||||||
|
self.pause()
|
||||||
|
|
||||||
|
|
||||||
|
class EditProductMenu(Menu):
|
||||||
|
def __init__(self):
|
||||||
|
Menu.__init__(self, 'Edit product', uses_db=True)
|
||||||
|
|
||||||
|
def _execute(self):
|
||||||
|
self.print_header()
|
||||||
|
product = self.input_product('Product> ')
|
||||||
|
self.printc('Editing product %s' % product.name)
|
||||||
|
while True:
|
||||||
|
selector = Selector('Do what with %s?' % product.name,
|
||||||
|
items=[('name', 'Edit name'),
|
||||||
|
('price', 'Edit price'),
|
||||||
|
('barcode', 'Edit barcode'),
|
||||||
|
('hidden', 'Edit hidden status'),
|
||||||
|
('store', 'Store')])
|
||||||
|
what = selector.execute()
|
||||||
|
if what == 'name':
|
||||||
|
product.name = self.input_str('Name[%s]> ' % product.name, Product.name_re, (1, product.name_length))
|
||||||
|
elif what == 'price':
|
||||||
|
product.price = self.input_int('Price[%s]> ' % product.price, (1, 100000))
|
||||||
|
elif what == 'barcode':
|
||||||
|
product.bar_code = self.input_str('Bar code[%s]> ' % product.bar_code, Product.bar_code_re, (8, 13))
|
||||||
|
elif what == 'hidden':
|
||||||
|
product.hidden = self.confirm('Hidden[%s]' % ("Y" if product.hidden else "N"), False)
|
||||||
|
elif what == 'store':
|
||||||
|
try:
|
||||||
|
self.session.commit()
|
||||||
|
print 'Product %s stored' % product.name
|
||||||
|
except sqlalchemy.exc.SQLAlchemyError, e:
|
||||||
|
print 'Could not store product %s: %s' % (product.name, e)
|
||||||
|
self.pause()
|
||||||
|
return
|
||||||
|
elif what is None:
|
||||||
|
print 'Edit aborted'
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
print 'What what?'
|
||||||
|
|
||||||
|
|
||||||
|
class AdjustStockMenu(Menu):
|
||||||
|
def __init__(self):
|
||||||
|
Menu.__init__(self, 'Adjust stock', uses_db=True)
|
||||||
|
|
||||||
|
def _execute(self):
|
||||||
|
self.print_header()
|
||||||
|
product = self.input_product('Product> ')
|
||||||
|
|
||||||
|
print 'The stock of this product is: %d ' % product.stock
|
||||||
|
print 'Write the number of products you have added to the stock'
|
||||||
|
print 'Alternatively, correct the stock for any mistakes'
|
||||||
|
add_stock = self.input_int('Added stock> ', (-1000, 1000))
|
||||||
|
print 'You added %d to the stock of %s' % (add_stock, product)
|
||||||
|
|
||||||
|
product.stock += add_stock
|
||||||
|
|
||||||
|
print 'The stock is now %d' % product.stock
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.session.commit()
|
||||||
|
print 'Stock is now stored'
|
||||||
|
self.pause()
|
||||||
|
except sqlalchemy.exc.SQLAlchemyError, e:
|
||||||
|
print 'Could not store stock: %s' % e
|
||||||
|
self.pause()
|
||||||
|
return
|
||||||
|
print 'The stock is now %d' % product.stock
|
||||||
|
|
||||||
|
|
||||||
|
class CleanupStockMenu(Menu):
|
||||||
|
def __init__(self):
|
||||||
|
Menu.__init__(self, 'Stock Cleanup', uses_db=True)
|
||||||
|
|
||||||
|
def _execute(self):
|
||||||
|
self.print_header()
|
||||||
|
|
||||||
|
products = self.session.query(Product).filter(Product.stock != 0).all()
|
||||||
|
|
||||||
|
print "Every product in stock will be printed."
|
||||||
|
print "Entering no value will keep current stock or set it to 0 if it is negative."
|
||||||
|
print "Entering a value will set current stock to that value."
|
||||||
|
print "Press enter to begin."
|
||||||
|
|
||||||
|
self.pause()
|
||||||
|
|
||||||
|
changed_products = []
|
||||||
|
|
||||||
|
for product in products:
|
||||||
|
oldstock = product.stock
|
||||||
|
product.stock = self.input_int(product.name, (0, 10000), default=max(0, oldstock))
|
||||||
|
self.session.add(product)
|
||||||
|
if oldstock != product.stock:
|
||||||
|
changed_products.append((product, oldstock))
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.session.commit()
|
||||||
|
print 'New stocks are now stored.'
|
||||||
|
self.pause()
|
||||||
|
except sqlalchemy.exc.SQLAlchemyError, e:
|
||||||
|
print 'Could not store stock: %s' % e
|
||||||
|
self.pause()
|
||||||
|
return
|
||||||
|
|
||||||
|
for p in changed_products:
|
||||||
|
print p[0].name, ".", p[1], "->", p[0].stock
|
|
@ -0,0 +1,99 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from .helpermenus import MessageMenu, Menu
|
||||||
|
|
||||||
|
|
||||||
|
class FAQMenu(Menu):
|
||||||
|
def __init__(self):
|
||||||
|
Menu.__init__(self, 'Frequently Asked Questions')
|
||||||
|
self.items = [MessageMenu('What is the meaning with this program?',
|
||||||
|
'''
|
||||||
|
We want to avoid keeping lots of cash in PVVVV\'s money box and to
|
||||||
|
make it easy to pay for stuff without using money. (Without using
|
||||||
|
money each time, that is. You do of course have to pay for the things
|
||||||
|
you buy eventually).
|
||||||
|
|
||||||
|
Dibbler stores a "credit" amount for each user. When you register a
|
||||||
|
purchase in Dibbler, this amount is decreased. To increase your
|
||||||
|
credit, add money to the money box and use "Adjust credit" to tell
|
||||||
|
Dibbler about it.
|
||||||
|
'''),
|
||||||
|
MessageMenu('Can I still pay for stuff using cash?',
|
||||||
|
'Yes. You can safely ignore this program completely.'),
|
||||||
|
MessageMenu('How do I exit from a submenu/dialog/thing?',
|
||||||
|
'Type "exit" or C-d.'),
|
||||||
|
MessageMenu('What does "." mean?',
|
||||||
|
'''
|
||||||
|
The "." character, known as "full stop" or "period", is most often
|
||||||
|
used to indicate the end of a sentence.
|
||||||
|
|
||||||
|
It is also used by Dibbler to indicate that the program wants you to
|
||||||
|
read some text before continuing. Whenever some output ends with a
|
||||||
|
line containing only a period, you should read the lines above and
|
||||||
|
then press enter to continue.
|
||||||
|
'''),
|
||||||
|
MessageMenu('Why is the user interface so terribly unintuitive?',
|
||||||
|
'''
|
||||||
|
Answer #1: It is not.
|
||||||
|
|
||||||
|
Answer #2: We are trying to compete with PVV\'s microwave oven in
|
||||||
|
userfriendliness.
|
||||||
|
|
||||||
|
Answer #3: YOU are unintuitive.
|
||||||
|
'''),
|
||||||
|
MessageMenu('Why is there no help command?',
|
||||||
|
'There is. Have you tried typing "help"?'),
|
||||||
|
MessageMenu('Where are the easter eggs? I tried saying "moo", but nothing happened.',
|
||||||
|
'Don\'t say "moo".'),
|
||||||
|
MessageMenu('Why does the program speak English when all the users are Norwegians?',
|
||||||
|
u'Godt spørsmål. Det virket sikkert som en god idé der og da.'),
|
||||||
|
MessageMenu('I found a bug; is there a reward?',
|
||||||
|
'''
|
||||||
|
No.
|
||||||
|
|
||||||
|
But if you are certain that it is a bug, not a feature, then you
|
||||||
|
should fix it (or better: force someone else to do it).
|
||||||
|
|
||||||
|
Follow this procedure:
|
||||||
|
|
||||||
|
1. Check out the Dibbler code from https://dev.pvv.ntnu.no/svn/dibbler
|
||||||
|
|
||||||
|
2. Fix the bug.
|
||||||
|
|
||||||
|
3. Check that the program still runs (and, preferably, that the bug is
|
||||||
|
in fact fixed).
|
||||||
|
|
||||||
|
4. Commit.
|
||||||
|
|
||||||
|
5. Update the running copy from svn:
|
||||||
|
|
||||||
|
$ su -
|
||||||
|
# su -l -s /bin/bash pvvvv
|
||||||
|
$ cd dibbler
|
||||||
|
$ svn up
|
||||||
|
|
||||||
|
6. Type "restart" in Dibbler to replace the running process by a new
|
||||||
|
one using the updated files.
|
||||||
|
'''),
|
||||||
|
MessageMenu('My question isn\'t listed here; what do I do?',
|
||||||
|
'''
|
||||||
|
DON\'T PANIC.
|
||||||
|
|
||||||
|
Follow this procedure:
|
||||||
|
|
||||||
|
1. Ask someone (or read the source code) and get an answer.
|
||||||
|
|
||||||
|
2. Check out the Dibbler code from https://dev.pvv.ntnu.no/svn/dibbler
|
||||||
|
|
||||||
|
3. Add your question (with answer) to the FAQ and commit.
|
||||||
|
|
||||||
|
4. Update the running copy from svn:
|
||||||
|
|
||||||
|
$ su -
|
||||||
|
# su -l -s /bin/bash pvvvv
|
||||||
|
$ cd dibbler
|
||||||
|
$ svn up
|
||||||
|
|
||||||
|
5. Type "restart" in Dibbler to replace the running process by a new
|
||||||
|
one using the updated files.
|
||||||
|
''')]
|
|
@ -0,0 +1,509 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from select import select
|
||||||
|
|
||||||
|
import conf
|
||||||
|
from db import User, Session
|
||||||
|
from helpers import search_user, search_product, safe_str, guess_data_type, argmax
|
||||||
|
from text_interface import context_commands, local_help_commands, help_commands, \
|
||||||
|
exit_commands
|
||||||
|
|
||||||
|
|
||||||
|
class ExitMenu(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Menu(object):
|
||||||
|
def __init__(self, name, items=None, prompt='> ',
|
||||||
|
return_index=True,
|
||||||
|
exit_msg=None, exit_confirm_msg=None, exit_disallowed_msg=None,
|
||||||
|
help_text=None, uses_db=False):
|
||||||
|
self.name = name
|
||||||
|
self.items = items if items is not None else []
|
||||||
|
self.prompt = prompt
|
||||||
|
self.return_index = return_index
|
||||||
|
self.exit_msg = exit_msg
|
||||||
|
self.exit_confirm_msg = exit_confirm_msg
|
||||||
|
self.exit_disallowed_msg = exit_disallowed_msg
|
||||||
|
self.help_text = help_text
|
||||||
|
self.context = None
|
||||||
|
self.header_format = '[%s]'
|
||||||
|
self.uses_db = uses_db
|
||||||
|
self.session = None
|
||||||
|
|
||||||
|
def exit_menu(self):
|
||||||
|
if self.exit_disallowed_msg is not None:
|
||||||
|
print self.exit_disallowed_msg
|
||||||
|
return
|
||||||
|
if self.exit_confirm_msg is not None:
|
||||||
|
if not self.confirm(self.exit_confirm_msg, default=True):
|
||||||
|
return
|
||||||
|
raise ExitMenu()
|
||||||
|
|
||||||
|
def at_exit(self):
|
||||||
|
if self.exit_msg:
|
||||||
|
print self.exit_msg
|
||||||
|
|
||||||
|
def set_context(self, string, display=True):
|
||||||
|
self.context = string
|
||||||
|
if self.context is not None and display:
|
||||||
|
print self.context
|
||||||
|
|
||||||
|
def add_to_context(self, string):
|
||||||
|
self.context += string
|
||||||
|
|
||||||
|
def printc(self, string):
|
||||||
|
print string
|
||||||
|
if self.context is None:
|
||||||
|
self.context = string
|
||||||
|
else:
|
||||||
|
self.context += '\n' + string
|
||||||
|
|
||||||
|
def show_context(self):
|
||||||
|
print self.header_format % self.name
|
||||||
|
if self.context is not None:
|
||||||
|
print self.context
|
||||||
|
|
||||||
|
def item_is_submenu(self, i):
|
||||||
|
return isinstance(self.items[i], Menu)
|
||||||
|
|
||||||
|
def item_name(self, i):
|
||||||
|
if self.item_is_submenu(i):
|
||||||
|
return self.items[i].name
|
||||||
|
elif isinstance(self.items[i], tuple):
|
||||||
|
return self.items[i][1]
|
||||||
|
else:
|
||||||
|
return self.items[i]
|
||||||
|
|
||||||
|
def item_value(self, i):
|
||||||
|
if isinstance(self.items[i], tuple):
|
||||||
|
return self.items[i][0]
|
||||||
|
if self.return_index:
|
||||||
|
return i
|
||||||
|
return self.items[i]
|
||||||
|
|
||||||
|
def input_str(self, prompt=None, regex=None, length_range=(None, None),
|
||||||
|
empty_string_is_none=False, timeout=None):
|
||||||
|
if prompt is None:
|
||||||
|
prompt = self.prompt
|
||||||
|
if regex is not None:
|
||||||
|
while True:
|
||||||
|
result = self.input_str(prompt, length_range=length_range,
|
||||||
|
empty_string_is_none=empty_string_is_none)
|
||||||
|
if result is None or re.match(regex + '$', result):
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
print 'Value must match regular expression "%s"' % regex
|
||||||
|
if length_range != (None, None):
|
||||||
|
while True:
|
||||||
|
result = self.input_str(prompt, empty_string_is_none=empty_string_is_none)
|
||||||
|
if result is None:
|
||||||
|
length = 0
|
||||||
|
else:
|
||||||
|
length = len(result)
|
||||||
|
if ((length_range[0] and length < length_range[0]) or
|
||||||
|
(length_range[1] and length > length_range[1])):
|
||||||
|
if length_range[0] and length_range[1]:
|
||||||
|
print 'Value must have length in range [%d,%d]' % length_range
|
||||||
|
elif length_range[0]:
|
||||||
|
print 'Value must have length at least %d' % length_range[0]
|
||||||
|
else:
|
||||||
|
print 'Value must have length at most %d' % length_range[1]
|
||||||
|
else:
|
||||||
|
return result
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# result = None
|
||||||
|
# It is replaced either way
|
||||||
|
if timeout:
|
||||||
|
# assuming line buffering
|
||||||
|
sys.stdout.write(safe_str(prompt))
|
||||||
|
sys.stdout.flush()
|
||||||
|
rlist, _, _ = select([sys.stdin], [], [], timeout)
|
||||||
|
if not rlist:
|
||||||
|
# timeout occurred, simulate empty line
|
||||||
|
result = ''
|
||||||
|
else:
|
||||||
|
result = unicode(raw_input(), conf.input_encoding).strip()
|
||||||
|
else:
|
||||||
|
result = unicode(raw_input(safe_str(prompt)), conf.input_encoding).strip()
|
||||||
|
except EOFError:
|
||||||
|
print 'quit'
|
||||||
|
self.exit_menu()
|
||||||
|
continue
|
||||||
|
if result in exit_commands:
|
||||||
|
self.exit_menu()
|
||||||
|
continue
|
||||||
|
if result in help_commands:
|
||||||
|
self.general_help()
|
||||||
|
continue
|
||||||
|
if result in local_help_commands:
|
||||||
|
self.local_help()
|
||||||
|
continue
|
||||||
|
if result in context_commands:
|
||||||
|
self.show_context()
|
||||||
|
continue
|
||||||
|
if self.special_input_options(result):
|
||||||
|
continue
|
||||||
|
if empty_string_is_none and result == '':
|
||||||
|
return None
|
||||||
|
return result
|
||||||
|
|
||||||
|
def special_input_options(self, result):
|
||||||
|
"""
|
||||||
|
Handles special, magic input for input_str
|
||||||
|
|
||||||
|
Override this in subclasses to implement magic menu
|
||||||
|
choices. Return True if str was some valid magic menu
|
||||||
|
choice, False otherwise.
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def special_input_choice(self, in_str):
|
||||||
|
"""
|
||||||
|
Handle choices which are not simply menu items.
|
||||||
|
|
||||||
|
Override this in subclasses to implement magic menu
|
||||||
|
choices. Return True if str was some valid magic menu
|
||||||
|
choice, False otherwise.
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def input_choice(self, number_of_choices, prompt=None):
|
||||||
|
if prompt is None:
|
||||||
|
prompt = self.prompt
|
||||||
|
while True:
|
||||||
|
result = self.input_str(prompt)
|
||||||
|
if result == '':
|
||||||
|
print 'Please enter something'
|
||||||
|
# 'c' in main menu to change colours
|
||||||
|
elif result == 'c':
|
||||||
|
os.system('echo -e "\033[' + str(random.randint(40, 49)) + ';' + str(random.randint(30, 37)) + ';5m"')
|
||||||
|
os.system('clear')
|
||||||
|
self.show_context()
|
||||||
|
|
||||||
|
# 'cs' in main menu to change colours back to default
|
||||||
|
elif result == 'cs':
|
||||||
|
os.system('echo -e "\033[0m"')
|
||||||
|
os.system('clear')
|
||||||
|
self.show_context()
|
||||||
|
|
||||||
|
else:
|
||||||
|
if result.isdigit():
|
||||||
|
choice = int(result)
|
||||||
|
if choice == 0 and 10 <= number_of_choices:
|
||||||
|
return 10
|
||||||
|
if 0 < choice <= number_of_choices:
|
||||||
|
return choice
|
||||||
|
if not self.special_input_choice(result):
|
||||||
|
self.invalid_menu_choice(result)
|
||||||
|
|
||||||
|
def invalid_menu_choice(self, in_str):
|
||||||
|
print 'Please enter a valid choice.'
|
||||||
|
|
||||||
|
def input_int(self, prompt=None, allowed_range=(None, None), null_allowed=False, default=None):
|
||||||
|
if prompt is None:
|
||||||
|
prompt = self.prompt
|
||||||
|
if default is not None:
|
||||||
|
prompt += ("[%s] " % default)
|
||||||
|
while True:
|
||||||
|
result = self.input_str(prompt)
|
||||||
|
if result == '':
|
||||||
|
if default is not None:
|
||||||
|
return default
|
||||||
|
elif null_allowed:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
value = int(result)
|
||||||
|
if ((allowed_range[0] and value < allowed_range[0]) or
|
||||||
|
(allowed_range[1] and value > allowed_range[1])):
|
||||||
|
if allowed_range[0] and allowed_range[1]:
|
||||||
|
print 'Value must be in range [%d,%d]' % allowed_range
|
||||||
|
elif allowed_range[0]:
|
||||||
|
print 'Value must be at least %d' % allowed_range[0]
|
||||||
|
else:
|
||||||
|
print 'Value must be at most %d' % allowed_range[1]
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
except ValueError:
|
||||||
|
print "Please enter an integer"
|
||||||
|
|
||||||
|
def input_user(self, prompt=None):
|
||||||
|
user = None
|
||||||
|
while user is None:
|
||||||
|
user = self.retrieve_user(self.input_str(prompt))
|
||||||
|
return user
|
||||||
|
|
||||||
|
def retrieve_user(self, search_str):
|
||||||
|
return self.search_ui(search_user, search_str, 'user')
|
||||||
|
|
||||||
|
def input_product(self, prompt=None):
|
||||||
|
product = None
|
||||||
|
while product is None:
|
||||||
|
product = self.retrieve_product(self.input_str(prompt))
|
||||||
|
return product
|
||||||
|
|
||||||
|
def retrieve_product(self, search_str):
|
||||||
|
return self.search_ui(search_product, search_str, 'product')
|
||||||
|
|
||||||
|
def input_thing(self, prompt=None, permitted_things=('user', 'product'),
|
||||||
|
add_nonexisting=(), empty_input_permitted=False, find_hidden_products=True):
|
||||||
|
result = None
|
||||||
|
while result is None:
|
||||||
|
search_str = self.input_str(prompt)
|
||||||
|
if search_str == '' and empty_input_permitted:
|
||||||
|
return None
|
||||||
|
result = self.search_for_thing(search_str, permitted_things, add_nonexisting, find_hidden_products)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def input_multiple(self, prompt=None, permitted_things=('user', 'product'),
|
||||||
|
add_nonexisting=(), empty_input_permitted=False, find_hidden_products=True):
|
||||||
|
result = None
|
||||||
|
num = 0
|
||||||
|
while result is None:
|
||||||
|
search_str = self.input_str(prompt)
|
||||||
|
search_lst = search_str.split(" ")
|
||||||
|
if search_str == '' and empty_input_permitted:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
result = self.search_for_thing(search_str, permitted_things, add_nonexisting, find_hidden_products)
|
||||||
|
num = 1
|
||||||
|
|
||||||
|
if (result is None) and (len(search_lst) > 1):
|
||||||
|
print 'Interpreting input as "<number> <product>"'
|
||||||
|
try:
|
||||||
|
num = int(search_lst[0])
|
||||||
|
result = self.search_for_thing(" ".join(search_lst[1:]), permitted_things, add_nonexisting,
|
||||||
|
find_hidden_products)
|
||||||
|
# Her kan det legges inn en except ValueError,
|
||||||
|
# men da blir det fort mye plaging av brukeren
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return result, num
|
||||||
|
|
||||||
|
def search_for_thing(self, search_str, permitted_things=('user', 'product'),
|
||||||
|
add_non_existing=(), find_hidden_products=True):
|
||||||
|
search_fun = {'user': search_user,
|
||||||
|
'product': search_product}
|
||||||
|
results = {}
|
||||||
|
result_values = {}
|
||||||
|
for thing in permitted_things:
|
||||||
|
results[thing] = search_fun[thing](search_str, self.session, find_hidden_products)
|
||||||
|
result_values[thing] = self.search_result_value(results[thing])
|
||||||
|
selected_thing = argmax(result_values)
|
||||||
|
if not results[selected_thing]:
|
||||||
|
thing_for_type = {'card': 'user', 'username': 'user',
|
||||||
|
'bar_code': 'product', 'rfid': 'rfid'}
|
||||||
|
type_guess = guess_data_type(search_str)
|
||||||
|
if type_guess is not None and thing_for_type[type_guess] in add_non_existing:
|
||||||
|
return self.search_add(search_str)
|
||||||
|
# print 'No match found for "%s".' % search_str
|
||||||
|
return None
|
||||||
|
return self.search_ui2(search_str, results[selected_thing], selected_thing)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def search_result_value(result):
|
||||||
|
if result is None:
|
||||||
|
return 0
|
||||||
|
if not isinstance(result, list):
|
||||||
|
return 3
|
||||||
|
if len(result) == 0:
|
||||||
|
return 0
|
||||||
|
if len(result) == 1:
|
||||||
|
return 2
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def search_add(self, string):
|
||||||
|
type_guess = guess_data_type(string)
|
||||||
|
if type_guess == 'username':
|
||||||
|
print '"%s" looks like a username, but no such user exists.' % string
|
||||||
|
if self.confirm('Create user %s?' % string):
|
||||||
|
user = User(string, None)
|
||||||
|
self.session.add(user)
|
||||||
|
return user
|
||||||
|
return None
|
||||||
|
if type_guess == 'card':
|
||||||
|
selector = Selector('"%s" looks like a card number, but no user with that card number exists.' % string,
|
||||||
|
[('create', 'Create user with card number %s' % string),
|
||||||
|
('set', 'Set card number of an existing user to %s' % string)])
|
||||||
|
selection = selector.execute()
|
||||||
|
if selection == 'create':
|
||||||
|
username = self.input_str('Username for new user (should be same as PVV username)> ',
|
||||||
|
User.name_re, (1, 10))
|
||||||
|
user = User(username, string)
|
||||||
|
self.session.add(user)
|
||||||
|
return user
|
||||||
|
if selection == 'set':
|
||||||
|
user = self.input_user('User to set card number for> ')
|
||||||
|
old_card = user.card
|
||||||
|
user.card = string
|
||||||
|
print 'Card number of %s set to %s (was %s)' % (user.name, string, old_card)
|
||||||
|
return user
|
||||||
|
return None
|
||||||
|
if type_guess == 'bar_code':
|
||||||
|
print '"%s" looks like the bar code for a product, but no such product exists.' % string
|
||||||
|
return None
|
||||||
|
|
||||||
|
def search_ui(self, search_fun, search_str, thing):
|
||||||
|
result = search_fun(search_str, self.session)
|
||||||
|
return self.search_ui2(search_str, result, thing)
|
||||||
|
|
||||||
|
def search_ui2(self, search_str, result, thing):
|
||||||
|
if not isinstance(result, list):
|
||||||
|
return result
|
||||||
|
if len(result) == 0:
|
||||||
|
print 'No %ss matching "%s"' % (thing, search_str)
|
||||||
|
return None
|
||||||
|
if len(result) == 1:
|
||||||
|
msg = 'One %s matching "%s": %s. Use this?' % \
|
||||||
|
(thing, search_str, unicode(result[0]))
|
||||||
|
if self.confirm(msg, default=True):
|
||||||
|
return result[0]
|
||||||
|
return None
|
||||||
|
limit = 9
|
||||||
|
if len(result) > limit:
|
||||||
|
select_header = '%d %ss matching "%s"; showing first %d' % \
|
||||||
|
(len(result), thing, search_str, limit)
|
||||||
|
select_items = result[:limit]
|
||||||
|
else:
|
||||||
|
select_header = '%d %ss matching "%s"' % \
|
||||||
|
(len(result), thing, search_str)
|
||||||
|
select_items = result
|
||||||
|
selector = Selector(select_header, items=select_items,
|
||||||
|
return_index=False)
|
||||||
|
return selector.execute()
|
||||||
|
|
||||||
|
def confirm(self, prompt, default=None, timeout=None):
|
||||||
|
return ConfirmMenu(prompt, default, timeout).execute()
|
||||||
|
|
||||||
|
def print_header(self):
|
||||||
|
print ""
|
||||||
|
print self.header_format % self.name
|
||||||
|
|
||||||
|
def pause(self):
|
||||||
|
self.input_str('.')
|
||||||
|
|
||||||
|
def general_help(self):
|
||||||
|
print '''
|
||||||
|
DIBBLER HELP
|
||||||
|
|
||||||
|
The following commands are recognized (almost) everywhere:
|
||||||
|
|
||||||
|
help, ? -- display this help
|
||||||
|
what, ?? -- redisplay the current context
|
||||||
|
help!, ??? -- display context-specific help (if any)
|
||||||
|
faq -- display frequently asked questions (with answers)
|
||||||
|
exit, quit, etc. -- exit from the current menu
|
||||||
|
|
||||||
|
When prompted for a user, you can type (parts of) the user name or
|
||||||
|
card number. When prompted for a product, you can type (parts of) the
|
||||||
|
product name or barcode.
|
||||||
|
|
||||||
|
About payment and "credit": When paying for something, use either
|
||||||
|
Dibbler or the good old money box -- never both at the same time.
|
||||||
|
Dibbler keeps track of a "credit" for each user, which is the amount
|
||||||
|
of money PVVVV owes the user. This value decreases with the
|
||||||
|
appropriate amount when you register a purchase, and you may increase
|
||||||
|
it by putting money in the box and using the "Adjust credit" menu.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def local_help(self):
|
||||||
|
if self.help_text is None:
|
||||||
|
print 'no help here'
|
||||||
|
else:
|
||||||
|
print ''
|
||||||
|
print 'Help for %s:' % (self.header_format % self.name)
|
||||||
|
print self.help_text
|
||||||
|
|
||||||
|
def execute(self, **kwargs):
|
||||||
|
self.set_context(None)
|
||||||
|
try:
|
||||||
|
if self.uses_db and not self.session:
|
||||||
|
self.session = Session()
|
||||||
|
return self._execute(**kwargs)
|
||||||
|
except ExitMenu:
|
||||||
|
self.at_exit()
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
if self.session is not None:
|
||||||
|
self.session.close()
|
||||||
|
self.session = None
|
||||||
|
|
||||||
|
def _execute(self, **kwargs):
|
||||||
|
line_format = '%' + str(len(str(len(self.items)))) + 'd ) %s'
|
||||||
|
while True:
|
||||||
|
self.print_header()
|
||||||
|
self.set_context(None)
|
||||||
|
if len(self.items) == 0:
|
||||||
|
self.printc('(empty menu)')
|
||||||
|
self.pause()
|
||||||
|
return None
|
||||||
|
for i in range(len(self.items)):
|
||||||
|
self.printc(line_format % (i + 1, self.item_name(i)))
|
||||||
|
item_i = self.input_choice(len(self.items), prompt=self.prompt) - 1
|
||||||
|
if self.item_is_submenu(item_i):
|
||||||
|
self.items[item_i].execute()
|
||||||
|
else:
|
||||||
|
return self.item_value(item_i)
|
||||||
|
|
||||||
|
|
||||||
|
class MessageMenu(Menu):
|
||||||
|
def __init__(self, name, message, pause_after_message=True):
|
||||||
|
Menu.__init__(self, name)
|
||||||
|
self.message = message.strip()
|
||||||
|
self.pause_after_message = pause_after_message
|
||||||
|
|
||||||
|
def _execute(self):
|
||||||
|
self.print_header()
|
||||||
|
print ''
|
||||||
|
print self.message
|
||||||
|
if self.pause_after_message:
|
||||||
|
self.pause()
|
||||||
|
|
||||||
|
|
||||||
|
class ConfirmMenu(Menu):
|
||||||
|
def __init__(self, prompt='confirm?', default=None, timeout=0):
|
||||||
|
Menu.__init__(self, 'question', prompt=prompt,
|
||||||
|
exit_disallowed_msg='Please answer yes or no')
|
||||||
|
self.default = default
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
|
def _execute(self):
|
||||||
|
options = {True: '[y]/n', False: 'y/[n]', None: 'y/n'}[self.default]
|
||||||
|
while True:
|
||||||
|
result = self.input_str('%s (%s) ' % (self.prompt, options), timeout=self.timeout)
|
||||||
|
result = result.lower().strip()
|
||||||
|
if result in ['y', 'yes']:
|
||||||
|
return True
|
||||||
|
elif result in ['n', 'no']:
|
||||||
|
return False
|
||||||
|
elif self.default is not None and result == '':
|
||||||
|
return self.default
|
||||||
|
else:
|
||||||
|
print 'Please answer yes or no'
|
||||||
|
|
||||||
|
|
||||||
|
class Selector(Menu):
|
||||||
|
def __init__(self, name, items=None, prompt='select> ', return_index=True, exit_msg=None, exit_confirm_msg=None,
|
||||||
|
help_text=None):
|
||||||
|
if items is None:
|
||||||
|
items = []
|
||||||
|
Menu.__init__(self, name, items, prompt, return_index, exit_msg)
|
||||||
|
self.header_format = '%s'
|
||||||
|
|
||||||
|
def print_header(self):
|
||||||
|
print self.header_format % self.name
|
||||||
|
|
||||||
|
def local_help(self):
|
||||||
|
if self.help_text is None:
|
||||||
|
print 'This is a selection menu. Enter one of the listed numbers, or'
|
||||||
|
print '\'exit\' to go out and do something else.'
|
||||||
|
else:
|
||||||
|
print ''
|
||||||
|
print 'Help for selector (%s):' % self.name
|
||||||
|
print self.help_text
|
|
@ -0,0 +1,49 @@
|
||||||
|
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import os
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from db import Session
|
||||||
|
from text_interface import faq_commands, restart_commands
|
||||||
|
from text_interface.buymenu import BuyMenu
|
||||||
|
from text_interface.faq import FAQMenu
|
||||||
|
from text_interface.helpermenus import Menu
|
||||||
|
|
||||||
|
|
||||||
|
def restart():
|
||||||
|
# Does not work if the script is not executable, or if it was
|
||||||
|
# started by searching $PATH.
|
||||||
|
os.execv(sys.argv[0], sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
class MainMenu(Menu):
|
||||||
|
def special_input_choice(self, in_str):
|
||||||
|
mv = in_str.split()
|
||||||
|
if len(mv) == 2 and mv[0].isdigit():
|
||||||
|
num = int(mv[0])
|
||||||
|
item_name = mv[1]
|
||||||
|
else:
|
||||||
|
num = 1
|
||||||
|
item_name = in_str
|
||||||
|
buy_menu = BuyMenu(Session())
|
||||||
|
thing = buy_menu.search_for_thing(item_name, find_hidden_products=False)
|
||||||
|
if thing:
|
||||||
|
buy_menu.execute(initial_contents=[(thing, num)])
|
||||||
|
self.show_context()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def special_input_options(self, result):
|
||||||
|
if result in faq_commands:
|
||||||
|
FAQMenu().execute()
|
||||||
|
return True
|
||||||
|
if result in restart_commands:
|
||||||
|
if self.confirm('Restart Dibbler?'):
|
||||||
|
restart()
|
||||||
|
pass
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def invalid_menu_choice(self, in_str):
|
||||||
|
print self.show_context()
|
|
@ -0,0 +1,212 @@
|
||||||
|
import conf
|
||||||
|
import sqlalchemy
|
||||||
|
from db import Transaction, Product, User
|
||||||
|
from helpers import less
|
||||||
|
from text_interface.helpermenus import Menu, Selector
|
||||||
|
|
||||||
|
|
||||||
|
class TransferMenu(Menu):
|
||||||
|
def __init__(self):
|
||||||
|
Menu.__init__(self, 'Transfer credit between users',
|
||||||
|
uses_db=True)
|
||||||
|
|
||||||
|
def _execute(self):
|
||||||
|
self.print_header()
|
||||||
|
amount = self.input_int('Transfer amount> ', (1, 100000))
|
||||||
|
self.set_context('Transfering %d kr' % amount, display=False)
|
||||||
|
user1 = self.input_user('From user> ')
|
||||||
|
self.add_to_context(' from ' + user1.name)
|
||||||
|
user2 = self.input_user('To user> ')
|
||||||
|
self.add_to_context(' to ' + user2.name)
|
||||||
|
|
||||||
|
t1 = Transaction(user1, amount,
|
||||||
|
'transfer to ' + user2.name)
|
||||||
|
t2 = Transaction(user2, -amount,
|
||||||
|
'transfer from ' + user1.name)
|
||||||
|
t1.perform_transaction()
|
||||||
|
t2.perform_transaction()
|
||||||
|
self.session.add(t1)
|
||||||
|
self.session.add(t2)
|
||||||
|
try:
|
||||||
|
self.session.commit()
|
||||||
|
print 'Transfered %d kr from %s to %s' % (amount, user1, user2)
|
||||||
|
print 'User %s\'s credit is now %d kr' % (user1, user1.credit)
|
||||||
|
print 'User %s\'s credit is now %d kr' % (user2, user2.credit)
|
||||||
|
except sqlalchemy.exc.SQLAlchemyError, e:
|
||||||
|
print 'Could not perform transfer: %s' % e
|
||||||
|
# self.pause()
|
||||||
|
|
||||||
|
|
||||||
|
class ShowUserMenu(Menu):
|
||||||
|
def __init__(self):
|
||||||
|
Menu.__init__(self, 'Show user', uses_db=True)
|
||||||
|
|
||||||
|
def _execute(self):
|
||||||
|
self.print_header()
|
||||||
|
user = self.input_user('User name, card number or RFID> ')
|
||||||
|
print 'User name: %s' % user.name
|
||||||
|
print 'Card number: %s' % user.card
|
||||||
|
print 'RFID: %s' % user.rfid
|
||||||
|
print 'Credit: %s kr' % user.credit
|
||||||
|
selector = Selector('What do you want to know about %s?' % user.name,
|
||||||
|
items=[('transactions', 'Recent transactions (List of last ' + str(
|
||||||
|
conf.user_recent_transaction_limit) + ')'),
|
||||||
|
('products', 'Which products %s has bought, and how many' % user.name),
|
||||||
|
('transactions-all', 'Everything (List of all transactions)')])
|
||||||
|
what = selector.execute()
|
||||||
|
if what == 'transactions':
|
||||||
|
self.print_transactions(user, conf.user_recent_transaction_limit)
|
||||||
|
elif what == 'products':
|
||||||
|
self.print_purchased_products(user)
|
||||||
|
elif what == 'transactions-all':
|
||||||
|
self.print_all_transactions(user)
|
||||||
|
else:
|
||||||
|
print 'What what?'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def print_all_transactions(user):
|
||||||
|
num_trans = len(user.transactions)
|
||||||
|
string = '%s\'s transactions (%d):\n' % (user.name, num_trans)
|
||||||
|
for t in user.transactions[::-1]:
|
||||||
|
string += ' * %s: %s %d kr, ' % \
|
||||||
|
(t.time.strftime('%Y-%m-%d %H:%M'),
|
||||||
|
{True: 'in', False: 'out'}[t.amount < 0],
|
||||||
|
abs(t.amount))
|
||||||
|
if t.purchase:
|
||||||
|
string += 'purchase ('
|
||||||
|
string += ', '.join(map(lambda e: e.product.name,
|
||||||
|
t.purchase.entries))
|
||||||
|
string += ')'
|
||||||
|
if t.penalty > 1:
|
||||||
|
string += ' * %dx penalty applied' % t.penalty
|
||||||
|
else:
|
||||||
|
string += t.description
|
||||||
|
string += '\n'
|
||||||
|
less(string)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def print_transactions(user, limit=10):
|
||||||
|
num_trans = len(user.transactions)
|
||||||
|
if num_trans <= limit:
|
||||||
|
string = '%s\'s transactions (%d):\n' % (user.name, num_trans)
|
||||||
|
else:
|
||||||
|
string = '%s\'s transactions (%d, showing only last %d):\n' % (user.name, num_trans, limit)
|
||||||
|
for t in user.transactions[-1:-limit - 1:-1]:
|
||||||
|
string += ' * %s: %s %d kr, ' % \
|
||||||
|
(t.time.strftime('%Y-%m-%d %H:%M'),
|
||||||
|
{True: 'in', False: 'out'}[t.amount < 0],
|
||||||
|
abs(t.amount))
|
||||||
|
if t.purchase:
|
||||||
|
string += 'purchase ('
|
||||||
|
string += ', '.join(map(lambda e: e.product.name,
|
||||||
|
t.purchase.entries))
|
||||||
|
string += ')'
|
||||||
|
if t.penalty > 1:
|
||||||
|
string += ' * %dx penalty applied' % t.penalty
|
||||||
|
else:
|
||||||
|
string += t.description
|
||||||
|
string += '\n'
|
||||||
|
less(string)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def print_purchased_products(user):
|
||||||
|
products = []
|
||||||
|
for ref in user.products:
|
||||||
|
product = ref.product
|
||||||
|
count = ref.count
|
||||||
|
products.append((product, count))
|
||||||
|
num_products = len(products)
|
||||||
|
if num_products == 0:
|
||||||
|
print 'No products purchased yet'
|
||||||
|
else:
|
||||||
|
text = ''
|
||||||
|
text += 'Products purchased:\n'
|
||||||
|
for product, count in products:
|
||||||
|
text += u'{0:<47} {1:>3}\n'.format(product.name, count)
|
||||||
|
less(text)
|
||||||
|
|
||||||
|
|
||||||
|
class UserListMenu(Menu):
|
||||||
|
def __init__(self):
|
||||||
|
Menu.__init__(self, 'User list', uses_db=True)
|
||||||
|
|
||||||
|
def _execute(self):
|
||||||
|
self.print_header()
|
||||||
|
user_list = self.session.query(User).all()
|
||||||
|
total_credit = self.session.query(sqlalchemy.func.sum(User.credit)).first()[0]
|
||||||
|
|
||||||
|
line_format = '%-12s | %6s\n'
|
||||||
|
hline = '---------------------\n'
|
||||||
|
text = ''
|
||||||
|
text += line_format % ('username', 'credit')
|
||||||
|
text += hline
|
||||||
|
for user in user_list:
|
||||||
|
text += line_format % (user.name, user.credit)
|
||||||
|
text += hline
|
||||||
|
text += line_format % ('total credit', total_credit)
|
||||||
|
less(text)
|
||||||
|
|
||||||
|
|
||||||
|
class AdjustCreditMenu(Menu): # reimplements ChargeMenu; these should be combined to one
|
||||||
|
def __init__(self):
|
||||||
|
Menu.__init__(self, 'Adjust credit', uses_db=True)
|
||||||
|
|
||||||
|
def _execute(self):
|
||||||
|
self.print_header()
|
||||||
|
user = self.input_user('User> ')
|
||||||
|
print 'User %s\'s credit is %d kr' % (user.name, user.credit)
|
||||||
|
self.set_context('Adjusting credit for user %s' % user.name, display=False)
|
||||||
|
print '(Note on sign convention: Enter a positive amount here if you have'
|
||||||
|
print 'added money to the PVVVV money box, a negative amount if you have'
|
||||||
|
print 'taken money from it)'
|
||||||
|
amount = self.input_int('Add amount> ', (-100000, 100000))
|
||||||
|
print '(The "log message" will show up in the transaction history in the'
|
||||||
|
print '"Show user" menu. It is not necessary to enter a message, but it'
|
||||||
|
print 'might be useful to help you remember why you adjusted the credit)'
|
||||||
|
description = self.input_str('Log message> ', length_range=(0, 50))
|
||||||
|
if description == '':
|
||||||
|
description = 'manually adjusted credit'
|
||||||
|
transaction = Transaction(user, -amount, description)
|
||||||
|
transaction.perform_transaction()
|
||||||
|
self.session.add(transaction)
|
||||||
|
try:
|
||||||
|
self.session.commit()
|
||||||
|
print 'User %s\'s credit is now %d kr' % (user.name, user.credit)
|
||||||
|
except sqlalchemy.exc.SQLAlchemyError, e:
|
||||||
|
print 'Could not store transaction: %s' % e
|
||||||
|
# self.pause()
|
||||||
|
|
||||||
|
|
||||||
|
class ProductListMenu(Menu):
|
||||||
|
def __init__(self):
|
||||||
|
Menu.__init__(self, 'Product list', uses_db=True)
|
||||||
|
|
||||||
|
def _execute(self):
|
||||||
|
self.print_header()
|
||||||
|
text = ''
|
||||||
|
product_list = self.session.query(Product).filter(Product.hidden.is_(False)).order_by(Product.stock.desc())
|
||||||
|
total_value = 0
|
||||||
|
for p in product_list:
|
||||||
|
total_value += p.price * p.stock
|
||||||
|
line_format = '%-15s | %5s | %-' + str(Product.name_length) + 's | %5s \n'
|
||||||
|
text += line_format % ('bar code', 'price', 'name', 'stock')
|
||||||
|
text += 78 * '-' + '\n'
|
||||||
|
for p in product_list:
|
||||||
|
text += line_format % (p.bar_code, p.price, p.name, p.stock)
|
||||||
|
text += 78 * '-' + '\n'
|
||||||
|
text += line_format % ('Total value', total_value, '', '',)
|
||||||
|
less(text)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductSearchMenu(Menu):
|
||||||
|
def __init__(self):
|
||||||
|
Menu.__init__(self, 'Product search', uses_db=True)
|
||||||
|
|
||||||
|
def _execute(self):
|
||||||
|
self.print_header()
|
||||||
|
self.set_context('Enter (part of) product name or bar code')
|
||||||
|
product = self.input_product()
|
||||||
|
print 'Result: %s, price: %d kr, bar code: %s, stock: %d, hidden: %s' % (product.name, product.price,
|
||||||
|
product.bar_code, product.stock,
|
||||||
|
("Y" if product.hidden else "N"))
|
||||||
|
# self.pause()
|
|
@ -0,0 +1,95 @@
|
||||||
|
import sqlalchemy
|
||||||
|
from db import PurchaseEntry, Product, User
|
||||||
|
from helpers import less
|
||||||
|
from sqlalchemy import desc
|
||||||
|
from sqlalchemy import func
|
||||||
|
from statistikkHelpers import statisticsTextOnly
|
||||||
|
from text_interface.helpermenus import Menu
|
||||||
|
|
||||||
|
__all__ = ["ProductPopularityMenu", "ProductRevenueMenu", "BalanceMenu", "LoggedStatisticsMenu"]
|
||||||
|
|
||||||
|
|
||||||
|
class ProductPopularityMenu(Menu):
|
||||||
|
def __init__(self):
|
||||||
|
Menu.__init__(self, 'Products by popularity', uses_db=True)
|
||||||
|
|
||||||
|
def _execute(self):
|
||||||
|
self.print_header()
|
||||||
|
text = ''
|
||||||
|
sub = \
|
||||||
|
self.session.query(PurchaseEntry.product_id,
|
||||||
|
func.count('*').label('purchase_count')) \
|
||||||
|
.group_by(PurchaseEntry.product_id) \
|
||||||
|
.subquery()
|
||||||
|
product_list = \
|
||||||
|
self.session.query(Product, sub.c.purchase_count) \
|
||||||
|
.outerjoin((sub, Product.product_id == sub.c.product_id)) \
|
||||||
|
.order_by(desc(sub.c.purchase_count)) \
|
||||||
|
.filter(sub.c.purchase_count is not None) \
|
||||||
|
.all()
|
||||||
|
line_format = '%10s | %-' + str(Product.name_length) + 's\n'
|
||||||
|
text += line_format % ('items sold', 'product')
|
||||||
|
text += '-' * 58 + '\n'
|
||||||
|
for product, number in product_list:
|
||||||
|
text += line_format % (number, product.name)
|
||||||
|
less(text)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductRevenueMenu(Menu):
|
||||||
|
def __init__(self):
|
||||||
|
Menu.__init__(self, 'Products by revenue', uses_db=True)
|
||||||
|
|
||||||
|
def _execute(self):
|
||||||
|
self.print_header()
|
||||||
|
text = ''
|
||||||
|
sub = \
|
||||||
|
self.session.query(PurchaseEntry.product_id,
|
||||||
|
func.count('*').label('purchase_count')) \
|
||||||
|
.group_by(PurchaseEntry.product_id) \
|
||||||
|
.subquery()
|
||||||
|
product_list = \
|
||||||
|
self.session.query(Product, sub.c.purchase_count) \
|
||||||
|
.outerjoin((sub, Product.product_id == sub.c.product_id)) \
|
||||||
|
.order_by(desc(sub.c.purchase_count * Product.price)) \
|
||||||
|
.filter(sub.c.purchase_count is not None) \
|
||||||
|
.all()
|
||||||
|
line_format = '%7s | %10s | %5s | %-' + str(Product.name_length) + 's\n'
|
||||||
|
text += line_format % ('revenue', 'items sold', 'price', 'product')
|
||||||
|
text += '-' * (31 + Product.name_length) + '\n'
|
||||||
|
for product, number in product_list:
|
||||||
|
text += line_format % (number * product.price, number, product.price, product.name)
|
||||||
|
less(text)
|
||||||
|
|
||||||
|
|
||||||
|
class BalanceMenu(Menu):
|
||||||
|
def __init__(self):
|
||||||
|
Menu.__init__(self, 'Total balance of PVVVV', uses_db=True)
|
||||||
|
|
||||||
|
def _execute(self):
|
||||||
|
self.print_header()
|
||||||
|
text = ''
|
||||||
|
total_value = 0
|
||||||
|
product_list = self.session.query(Product).all()
|
||||||
|
for p in product_list:
|
||||||
|
total_value += p.stock * p.price
|
||||||
|
|
||||||
|
total_credit = self.session.query(sqlalchemy.func.sum(User.credit)).first()[0]
|
||||||
|
total_balance = total_value - total_credit
|
||||||
|
|
||||||
|
line_format = '%15s | %5d \n'
|
||||||
|
text += line_format % ('Total value', total_value)
|
||||||
|
text += 24 * '-' + '\n'
|
||||||
|
text += line_format % ('Total credit', total_credit)
|
||||||
|
text += 24 * '-' + '\n'
|
||||||
|
text += line_format % ('Total balance', total_balance)
|
||||||
|
less(text)
|
||||||
|
|
||||||
|
|
||||||
|
class LoggedStatisticsMenu(Menu):
|
||||||
|
def __init__(self):
|
||||||
|
Menu.__init__(self, 'Statistics from log', uses_db=True)
|
||||||
|
|
||||||
|
def _execute(self):
|
||||||
|
statisticsTextOnly()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue