Database format update. Products now have a "hidden" column.

Hidden products will not appear when searching for products by name outside of the Edit Product or Product Search menu.
Products can be marked as hidden in the Edit Product menu. This menu now also allows changing of barcodes, and the prompt contains the previous values.
Statistics no longer throw an error.
Minor clarity improvements for messages
    "New Price" only appears when product price actually changes
    Better message at the end of adding products to stock
Removed unnecessary pauses after some menu options
This commit is contained in:
robertem 2017-02-04 23:37:45 +00:00
parent dd3967e67d
commit 11593a71d0
3 changed files with 119 additions and 100 deletions

6
db.py
View File

@ -47,19 +47,21 @@ class Product(Base):
name = Column(String(45))
price = Column(Integer)
stock = Column(Integer)
hidden = Column(Boolean, nullable=False, default=False)
bar_code_re = r"[0-9]+"
name_re = r".+"
name_length = 45
def __init__(self, bar_code, name, price, stock=0):
def __init__(self, bar_code, name, price, stock=0, hidden = False):
self.name = name
self.bar_code = bar_code
self.price = price
self.stock = stock
self.hidden = hidden
def __repr__(self):
return "<Product('%s', '%s', '%s', '%s')>" % (self.name, self.bar_code, self.price, self.stock)
return "<Product('%s', '%s', '%s', '%s', '%s')>" % (self.name, self.bar_code, self.price, self.stock, self.hidden)
def __str__(self):
return self.name

View File

@ -1,54 +1,60 @@
from db import *
from sqlalchemy import or_
from sqlalchemy import or_, and_
import pwd
import subprocess
import os
import signal
def search_user(string, session):
string = string.lower()
exact_match = session.query(User).filter(or_(User.name==string, User.card==string, User.rfid==string)).first()
if exact_match:
return exact_match
user_list = session.query(User).filter(or_(User.name.ilike('%'+string+'%'),
User.card.ilike('%'+string+'%'),
User.rfid.ilike('%'+string+'%'))).all()
return user_list
def search_user(string, session, ignorethisflag=None):
string = string.lower()
exact_match = session.query(User).filter(or_(User.name==string, User.card==string, User.rfid==string)).first()
if exact_match:
return exact_match
user_list = session.query(User).filter(or_(User.name.ilike('%'+string+'%'),
User.card.ilike('%'+string+'%'),
User.rfid.ilike('%'+string+'%'))).all()
return user_list
def search_product(string, session):
exact_match = session.query(Product)\
.filter(or_(Product.bar_code==string,
Product.name==string)).first()
if exact_match:
return exact_match
product_list = session.query(Product)\
.filter(or_(Product.bar_code.ilike('%'+string+'%'),
Product.name.ilike('%'+string+'%'))).all()
return product_list
def search_product(string, session, find_hidden_products=True):
if find_hidden_products:
exact_match = session.query(Product).filter(or_(Product.bar_code==string, Product.name==string)).first()
else:
exact_match = session.query(Product).filter(or_(Product.bar_code==string,
and_(Product.name==string, Product.hidden == False))).first()
if exact_match:
return exact_match
if find_hidden_products:
product_list = session.query(Product).filter(or_(Product.bar_code.ilike('%'+string+'%'),
Product.name.ilike('%'+string+'%'))).all()
else:
product_list = session.query(Product).filter(or_(Product.bar_code.ilike('%' + string + '%'),
and_(Product.name.ilike('%' + string + '%'),
Product.hidden == False))).all()
return product_list
def system_user_exists(username):
try:
pwd.getpwnam(username)
except KeyError:
return False
except UnicodeEncodeError:
return False
else:
return True
try:
pwd.getpwnam(username)
except KeyError:
return False
except UnicodeEncodeError:
return False
else:
return True
def guess_data_type(string):
if string.startswith('ntnu') and string[4:].isdigit():
return 'card'
if string.isdigit() and len(string) == 10:
return 'rfid'
if string.isdigit() and len(string) in [8,13]:
return 'bar_code'
if string.startswith('ntnu') and string[4:].isdigit():
return 'card'
if string.isdigit() and len(string) == 10:
return 'rfid'
if string.isdigit() and len(string) in [8,13]:
return 'bar_code'
# if string.isdigit() and len(string) > 5:
# return 'card'
if string.isalpha() and string.islower() and system_user_exists(string):
return 'username'
return None
if string.isalpha() and string.islower() and system_user_exists(string):
return 'username'
return None
# def retrieve_user(string, session):
@ -70,7 +76,7 @@ def guess_data_type(string):
# else:
# print "Found "+str(len(search))+" users:"
# return select_from_list(search)
# def confirm(prompt='Confirm? (y/n) '):
# while True:
@ -93,43 +99,43 @@ def guess_data_type(string):
# return None
def argmax(d, all=False, value=None):
maxarg = None
maxargs = []
if value != None:
dd = d
d = {}
for key in dd.keys():
d[key] = value(dd[key])
for key in d.keys():
if maxarg == None or d[key] > d[maxarg]:
maxarg = key
if all:
return filter(lambda k: d[k] == d[maxarg], d.keys())
return maxarg
maxarg = None
maxargs = []
if value != None:
dd = d
d = {}
for key in dd.keys():
d[key] = value(dd[key])
for key in d.keys():
if maxarg == None or d[key] > d[maxarg]:
maxarg = key
if all:
return filter(lambda k: d[k] == d[maxarg], d.keys())
return maxarg
def safe_str(obj):
'''
Ugly hack to avoid Python complaining about encodings.
'''
Ugly hack to avoid Python complaining about encodings.
Call this on any object to turn it into a string which is
(hopefully) safe for printing.
'''
if isinstance(obj, str):
return obj
if isinstance(obj, unicode):
return obj.encode('utf8')
else:
return safe_str(unicode(obj))
Call this on any object to turn it into a string which is
(hopefully) safe for printing.
'''
if isinstance(obj, str):
return obj
if isinstance(obj, unicode):
return obj.encode('utf8')
else:
return safe_str(unicode(obj))
def less(string):
'''
Run less with string as input; wait until it finishes.
'''
# If we don't ignore SIGINT while running the `less` process,
# it will become a zombie when someone presses C-c.
int_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
env = dict(os.environ)
env['LESSSECURE'] = '1'
proc = subprocess.Popen('less', env=env, stdin=subprocess.PIPE)
proc.communicate(safe_str(string))
signal.signal(signal.SIGINT, int_handler)
'''
Run less with string as input; wait until it finishes.
'''
# If we don't ignore SIGINT while running the `less` process,
# it will become a zombie when someone presses C-c.
int_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
env = dict(os.environ)
env['LESSSECURE'] = '1'
proc = subprocess.Popen('less', env=env, stdin=subprocess.PIPE)
proc.communicate(safe_str(string))
signal.signal(signal.SIGINT, int_handler)

View File

@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
import sqlalchemy
from sqlalchemy import distinct
from sqlalchemy.sql import func
from sqlalchemy import desc
import re, sys, os, traceback, signal, readline
@ -253,17 +254,17 @@ class Menu():
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):
add_nonexisting=(), empty_input_permitted=False, find_hidden_products=True):
result = None
while result == 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)
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):
add_nonexisting=(), empty_input_permitted=False, find_hidden_products=True):
result=None
while result == None:
search_str = self.input_str(prompt)
@ -271,14 +272,14 @@ class Menu():
if search_str == '' and empty_input_permitted:
return None
else:
result = self.search_for_thing(search_str, permitted_things, add_nonexisting)
result = self.search_for_thing(search_str, permitted_things, add_nonexisting, find_hidden_products)
num = 1
if (result == 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)
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:
@ -287,13 +288,13 @@ class Menu():
def search_for_thing(self, search_str, permitted_things=('user','product'),
add_nonexisting=()):
add_nonexisting=(), 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)
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 results[selected_thing] == []:
@ -637,7 +638,7 @@ class TransferMenu(Menu):
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()
#self.pause()
class AddUserMenu(Menu):
@ -727,13 +728,19 @@ class EditProductMenu(Menu):
while True:
selector = Selector('Do what with %s?' % product.name,
items=[('name', 'Edit name'),
('price', 'Edit price (currently %d)' % product.price),
('price', 'Edit price'),
('barcode', 'Edit barcode'),
('hidden', 'Edit hidden status'),
('store', 'Store')])
what = selector.execute()
if what == 'name':
product.name = self.input_str('Name> ', Product.name_re, (1,product.name_length))
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> ', (1,100000))
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()
@ -966,7 +973,8 @@ When finished, write an empty line to confirm the purchase.
# Read in a 'thing' (product or user):
thing = self.input_thing(add_nonexisting=('user',),
empty_input_permitted=True)
empty_input_permitted=True,
find_hidden_products=False)
# Possibly exit from the menu:
if thing == None:
@ -1148,7 +1156,7 @@ class AdjustCreditMenu(Menu): # reimplements ChargeMenu; these should be combine
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()
#self.pause()
class ProductListMenu(Menu):
@ -1158,7 +1166,7 @@ class ProductListMenu(Menu):
def _execute(self):
self.print_header()
text = ''
product_list = self.session.query(Product).all()
product_list = self.session.query(Product).filter(Product.hidden == False)
total_value = 0
for p in product_list:
total_value += p.price*p.stock
@ -1180,8 +1188,10 @@ class ProductSearchMenu(Menu):
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' % (product.name, product.price, product.bar_code, product.stock)
self.pause()
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()
class ProductPopularityMenu(Menu):
@ -1192,13 +1202,13 @@ class ProductPopularityMenu(Menu):
self.print_header()
text = ''
sub = \
self.session.query(PurchaseEntry.product_bar_code,
self.session.query(PurchaseEntry.product_id,
func.count('*').label('purchase_count')) \
.group_by(PurchaseEntry.product_bar_code) \
.group_by(PurchaseEntry.product_id) \
.subquery()
product_list = \
self.session.query(Product, sub.c.purchase_count) \
.outerjoin((sub, Product.bar_code==sub.c.product_bar_code)) \
.outerjoin((sub, Product.product_id==sub.c.product_id)) \
.order_by(desc(sub.c.purchase_count)) \
.filter(sub.c.purchase_count != None) \
.all()
@ -1217,13 +1227,13 @@ class ProductRevenueMenu(Menu):
self.print_header()
text = ''
sub = \
self.session.query(PurchaseEntry.product_bar_code,
self.session.query(PurchaseEntry.product_id,
func.count('*').label('purchase_count')) \
.group_by(PurchaseEntry.product_bar_code) \
.group_by(PurchaseEntry.product_id) \
.subquery()
product_list = \
self.session.query(Product, sub.c.purchase_count) \
.outerjoin((sub, Product.bar_code==sub.c.product_bar_code)) \
.outerjoin((sub, Product.product_id==sub.c.product_id)) \
.order_by(desc(sub.c.purchase_count*Product.price)) \
.filter(sub.c.purchase_count != None) \
.all()
@ -1317,7 +1327,7 @@ much money you're due in credits for the purchase when prompted.
thing_price = 0
# Read in a 'thing' (product or user):
line = self.input_multiple(add_nonexisting=('user','product'), empty_input_permitted=True)
line = self.input_multiple(add_nonexisting=('user','product'), empty_input_permitted=True, find_hidden_products=False)
if line:
(thing, amount) = line
@ -1344,7 +1354,7 @@ much money you're due in credits for the purchase when prompted.
# 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 total amount?', default=False):
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()
@ -1392,9 +1402,10 @@ much money you're due in credits for the purchase when prompted.
self.session.add(transaction)
for product in self.products:
value = max(product.stock, 0)*product.price + self.products[product][1]
old_price = product.price
product.price = int(ceil(float(value)/(max(product.stock, 0) + self.products[product][0])))
product.stock += self.products[product][0]
print "New stock for", product.name, "- New price:", product.price
print "New stock for %s: %d" % (product.name, product.stock), ("- New price: " + str(product.price) if old_price != product.price else "")
try:
self.session.commit()
print "Success! Transaction performed:"