here it is my simple cash register app. It lacks some functionality ( class Payments
should have more options like daily statements etc, class TillSettings
needs more settings as well) but it does the job. Simple Cash Register and Stock Manager allows to add items to stock and then sell it and register total value of sales (very basic options). There is one bug I haven't fixed yet but that is a matter just of time. My question is do you have any advice for beginner in building 'bigger' apps. I mean that could be extended for multiple options, clerk id, manager privileges, refunds etc but have I started anywhere close to the right way? Any advice and critic will be appreciated. This is only one of the projects for me to learn so having my code seen and commented on by people who know how to do these things is important to me.
import tkinter as tk
import os, json
from typing import Any
from tkinter import DoubleVar, StringVar, ttk
from tkinter.constants import CENTER, END, NO
class Application(tk.Tk):
def __init__(self, database: 'StockDatabase', settings: 'TillSettings', payments: 'Payments') -> None:
super().__init__()
self._db = database
self._sett = settings
self._pay = payments
self.geometry('500x500')
self.geometry('+100+100')
self.title('Cash Register')
self.iconbitmap('icon.ico')
self.createButtons()
self.createPurchaseList()
self.createLabels()
def createButtons(self) -> None:
addToCart = ttk.Button(self, text='Add to basket', command=self.addToBasket)
addToCart.place(x=10, y=10, width=90, height=50)
stockManagment = ttk.Button(self, text='Manage Stock', command=self.manageStockWindow)
stockManagment.place(x=10, y=430, width=90)
exitButton = ttk.Button(self, text='EXIT', command= self.destroy)
exitButton.place(x=10, y=460, width=90)
paymentButton = ttk.Button(self, text='PAY', command=self.getPayment)
paymentButton.place(x=390, y=420, height=70, width=90)
def createPurchaseList(self) -> None:
columns = ('#1', '#2', '#3', '#4')
# set shopping list panel
self.shoppingList = ttk.Treeview(self, columns=columns, show='headings', height=27, selectmode='browse')
self.shoppingList.place(x=150, y=10, width=330, height=400)
self.shoppingList.heading('#1', text='Product')
self.shoppingList.heading('#2', text='#') # quantity
self.shoppingList.heading('#3', text='#') # price per item
self.shoppingList.heading('#4', text='Sum')
self.shoppingList.column('#1', anchor=CENTER, stretch=NO, width=157)
self.shoppingList.column('#2', anchor=CENTER, stretch=NO, width=50)
self.shoppingList.column('#3', anchor=CENTER, stretch=NO, width=50)
self.shoppingList.column('#4', anchor=CENTER, stretch=NO, width=70)
# set scrollbar for shopping list
scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.shoppingList.yview)
self.shoppingList.configure(yscrollcommand=scrollbar.set)
scrollbar.place(x=481, y=10, height=400)
def manageStockWindow(self) -> None:
mngStockWin = tk.Toplevel()
mngStockWin.geometry('350x600')
mngStockWin.geometry('+150+100')
mngStockWin.title('Stock Manager')
mngStockWin.iconbitmap('icon.ico')
def addNewItem() -> None:
newItemWin = tk.Toplevel()
newItemWin.geometry('300x200')
newItemWin.geometry('+200+200')
newItemWin.title('New Item Menu')
newItemWin.iconbitmap('icon.ico')
# set labels
nameLabel = ttk.Label(newItemWin, text='Product:')
nameLabel.place(x=10, y=10)
priceLabel = ttk.Label(newItemWin, text='Price:')
priceLabel.place(x=10, y=30)
purchaseLabel = ttk.Label(newItemWin, text='Purchase Price:')
purchaseLabel.place(x=10, y=50)
quantityLabel = ttk.Label(newItemWin, text='Quantity:')
quantityLabel.place(x=10, y=70)
# set entry points
tk.nameVar = StringVar(value='Product')
nameEntry = ttk.Entry(newItemWin, textvariable=tk.nameVar, justify='right')
nameEntry.place(x=150, y=10)
tk.priceVar = DoubleVar(value=0.0)
priceEntry = ttk.Entry(newItemWin, textvariable=tk.priceVar, justify='right')
priceEntry.place(x=150, y=30)
tk.purchaseVar = DoubleVar(value=0.0)
purchaseEntry = ttk.Entry(newItemWin, textvariable=tk.purchaseVar, justify='right')
purchaseEntry.place(x=150, y=50)
tk.quantityVar = DoubleVar(value=0.0)
quantityEntry = ttk.Entry(newItemWin, textvariable=tk.quantityVar, justify='right')
quantityEntry.place(x=150, y=70)
# window buttons
cancelButton = ttk.Button(newItemWin, text='CANCEL', command=newItemWin.destroy)
cancelButton.place(x=190, y=150, height=30)
addButton = ttk.Button(newItemWin, text='ADD ITEM', command=lambda: [StockDatabase.addNewStockItem(self._db, [tk.nameVar.get(),
tk.priceVar.get(),
tk.purchaseVar.get(),
tk.quantityVar.get()]),
newItemWin.destroy(),
mngStockWin.destroy(),
self.manageStockWindow()])
addButton.place(x=30, y=150, height=30)
cancelButton = ttk.Button(mngStockWin, text='CANCEL', command=mngStockWin.destroy)
cancelButton.place(x=250, y=550, height=30)
addButton = ttk.Button(mngStockWin, text='ADD', command=addNewItem)
addButton.place(x=10, y=550, height=30)
# msw = Manage Stock Window
mswColumns = ('#1', '#2', '#3', '#4')
stockList = ttk.Treeview(mngStockWin, columns=mswColumns, show='headings', height=27, selectmode='browse')
stockList.place(x=10, y=10, width=315, height=500)
stockList.heading('#1', text='Product')
stockList.heading('#2', text='Price')
stockList.heading('#3', text='Purchase Price')
stockList.heading('#4', text='Quantity')
stockList.column('#1', anchor=CENTER, stretch=NO, width=133)
stockList.column('#2', anchor=CENTER, stretch=NO, width=60)
stockList.column('#3', anchor=CENTER, stretch=NO, width=60)
stockList.column('#4', anchor=CENTER, stretch=NO, width=60)
scrollbar = ttk.Scrollbar(mngStockWin, orient=tk.VERTICAL, command=self.shoppingList.yview)
stockList.configure(yscrollcommand=scrollbar.set)
scrollbar.place(x=325, y=10, height=500)
# insert data to stock list
for item in StockDatabase.loadData(self._db):
stockList.insert('', tk.END, values=item)
def deleteItem() -> None:
# get choosen item
currentItem = stockList.focus()
itemInfo = stockList.item(currentItem)
itemDetails = itemInfo["values"]
# excepting index error if someone would press delete when stock list is empty
try:
# corrected for 'for' loop comparison. .focus() .item() returned all values as str
correctedType = (itemDetails[0], float(itemDetails[1]), float(itemDetails[2]), float(itemDetails[3]))
# delete item
newData: list[Any] = []
for item in StockDatabase.loadData(self._db):
if item != correctedType:
newData.append(item)
StockDatabase.createNewStockFile(self._db)
for item in newData:
StockDatabase.addNewStockItem(self._db, item)
# refresh stock list
mngStockWin.destroy()
self.manageStockWindow()
except IndexError:
pass
# delete button
deleteButton = ttk.Button(mngStockWin, text='DELETE', command=deleteItem)
deleteButton.place(x=170, y=550, height=30)
def amendItemDetails() -> None:
amendWin = tk.Toplevel()
amendWin.geometry('250x250')
amendWin.geometry('+200+200')
amendWin.title('Change details')
amendWin.iconbitmap('icon.ico')
questionLabel = ttk.Label(amendWin, text='What would you like to change?')
questionLabel.place(x=10, y=10)
itemOptions = ('Sale Price', 'Purchase Price', 'Stock Quantity')
choice = StringVar()
options = ttk.OptionMenu(amendWin,
choice,
itemOptions[0],
*itemOptions)
options.place(x=10, y=47)
options.config(width=15)
itemNewData = ttk.Label(amendWin, text='New Data:')
itemNewData.place(x=10, y=90)
newDataEntry = ttk.Entry(amendWin, justify='right')
newDataEntry.place(x=10, y= 130)
def acceptNewData(detailIndex: int, newDetail: Any) -> None:
# get choosen item
currentItem = stockList.focus()
itemInfo = stockList.item(currentItem)
itemDetails = itemInfo["values"]
print(itemDetails)
# corrected for 'for' loop comparison. .focus() .item() returned all values as str
correctedType = (itemDetails[0], float(itemDetails[1]), float(itemDetails[2]), float(itemDetails[3]))
print(correctedType)
newData: list[Any] = []
for item in StockDatabase.loadData(self._db):
if item != correctedType:
print(item)
newData.append(item)
# append changed item
itemDetails[detailIndex] = float(newDetail)
# type adjustment as floats were turned into strings
itemDetails = (itemDetails[0], float(itemDetails[1]), float(itemDetails[2]), float(itemDetails[3]))
newData.append(itemDetails)
print(itemDetails)
print(correctedType)
StockDatabase.createNewStockFile(self._db)
# write to database
for item in newData:
StockDatabase.addNewStockItem(self._db, item)
# destroy window after changing details
amendWin.destroy()
# refresh stock list
mngStockWin.destroy()
self.manageStockWindow()
okButton = ttk.Button(amendWin, text='UPDATE', command=lambda: acceptNewData(itemOptions.index(choice.get()) + 1, newDataEntry.get()))
okButton.place(x=60, y=200)
cancelButton = ttk.Button(amendWin, text='CANCEL', command=amendWin.destroy)
cancelButton.place(x=150, y=200)
amendButton = ttk.Button(mngStockWin, text='CHANGE', command=amendItemDetails)
amendButton.place(x=90, y=550, height=30)
def addToBasket(self) -> None:
basketWin = tk.Toplevel()
basketWin.geometry('250x200')
basketWin.geometry('+200+200')
basketWin.title('Add Item to Basket')
basketWin.iconbitmap('icon.ico')
itemLabel = ttk.Label(basketWin, text='Item:')
itemLabel.place(x=10, y=10)
quantityLabel = ttk.Label(basketWin, text='Quantity:')
quantityLabel.place(x=10, y=70)
availableLAbel = ttk.Label(basketWin, text='Available:')
availableLAbel.place(x=10, y=110)
# load options from available stock
def getOptions() -> list[str]:
choice: list[str] = []
for item in StockDatabase.loadData(self._db):
choice.append(item[0])
if len(choice) == 0:
choice = ['Nothing in stock']
return choice
tk.choosenItem = StringVar()
options = ttk.OptionMenu(basketWin,
tk.choosenItem,
getOptions()[0],
*getOptions())
options.place(x=120, y=10)
options.config(width=15)
# get maximum quantity of item from stock database
def getMaxQuantity(item: str) -> float:
for product in StockDatabase.loadData(self._db):
if product[0] == item:
return float(product[3])
tk.quantityVar = DoubleVar(value=getMaxQuantity(tk.choosenItem.get()))
quantityAmountLabel = ttk.Label(basketWin, textvariable=tk.quantityVar, justify='right', background='lightgrey')
quantityAmountLabel.place(x=215, y=110)
tk.entryVar = DoubleVar()
quantityEntry = ttk.Entry(basketWin, textvariable=tk.entryVar, justify='right')
quantityEntry.place(x=120, y=70, width=120)
cancelButton = ttk.Button(basketWin, text='CANCEL', command=basketWin.destroy)
cancelButton.place(x=140, y=150)
# insert maximum available stock in quantity label, update everytime different option from option menu is choosen
# x, y, z just to pass argument to callback function, as required by tkinter. otherwise= error
def updateQuantity(x: Any, y: Any, z: Any) -> None:
tk.quantityVar = DoubleVar(value=getMaxQuantity(tk.choosenItem.get()))
quantityAmountLabel['textvariable'] = tk.quantityVar
tk.choosenItem.trace_add('write', updateQuantity)
'''NEED TO WORK ON THIS PART
EVERYTHING IS WORKING FINE, BUT WHILE ADDING SAME ITEM MULTIPLE TIMES
WHEN IT COMES TO PAYMENT TIME stock_data.json IS NOT UPDATED CORRECTLY.
NEED TO FIND THE WAY TO CONSOLIDATE ITEMS IN THE shoppingList'''
# create item for the shopping list, calculate sum for product (quantity * price)
def addToShoppingList(item: str, quantity: float) -> None:
productToAdd: list[Any] = []
productToAdd.append(item)
# excepting TypeError if someone will try to add items when stock_data is empty
try:
# ensure it can't be added more than in the stock
if quantity >= getMaxQuantity(tk.choosenItem.get()):
productToAdd.append(getMaxQuantity(tk.choosenItem.get()))
else:
productToAdd.append(quantity)
for product in StockDatabase.loadData(self._db):
if product[0] == item:
productToAdd.append(product[1])
total = productToAdd[1] * productToAdd[2]
productToAdd.append(total)
self.shoppingList.insert('', tk.END, values=productToAdd)
except TypeError:
pass
addButton = ttk.Button(basketWin, text='ADD', command=lambda: addToShoppingList(tk.choosenItem.get(), tk.entryVar.get()))
addButton.place(x=30, y=150)
def createLabels(self) -> None:
totalLabel = ttk.Label(self, text='TOTAL:')
totalLabel.place(x=150, y=420)
taxLabel = ttk.Label(self, text='TAX:')
taxLabel.place(x=150, y=445)
toPayLabel = ttk.Label(self, text='TOTAL TO PAY:')
toPayLabel.place(x=150, y=470)
def calculateTotal() -> float:
total: float = 0.0
for child in self.shoppingList.get_children():
total += float(self.shoppingList.item(child, 'values')[3])
return total
def getTaxValue() -> float:
for item in TillSettings.loadSettings(self._sett):
for key, value in item.items():
if key == 'sales tax':
return value
return 0.0
# just a basic tax calculation for training purposes
def calculateTax(amount: float) -> float:
return round((amount / 100) * getTaxValue(), 2)
self.totalVar = DoubleVar(value=calculateTotal())
totalValue = ttk.Label(self, textvariable=self.totalVar, background='lightgrey')
totalValue.place(x=330, y=420)
self.taxVar = DoubleVar(value=calculateTax(self.totalVar.get()))
taxValue = ttk.Label(self, textvariable=self.taxVar, background='lightgrey')
taxValue.place(x=330, y=445)
self.toPayVar = DoubleVar(value=self.taxVar.get() + self.totalVar.get())
toPayValue = ttk.Label(self, textvariable=self.toPayVar, background='lightgrey')
toPayValue.place(x=330, y=470)
def updateTotals() -> None:
self.totalVar = DoubleVar(value=calculateTotal())
self.taxVar = DoubleVar(value=calculateTax(self.totalVar.get()))
self.toPayVar = DoubleVar(value=self.taxVar.get() + self.totalVar.get())
totalValue = ttk.Label(self, textvariable=self.totalVar, background='lightgrey')
totalValue.place(x=330, y=420)
taxValue = ttk.Label(self, textvariable=self.taxVar, background='lightgrey')
taxValue.place(x=330, y=445)
toPayValue = ttk.Label(self, textvariable=self.toPayVar, background='lightgrey')
toPayValue.place(x=330, y=470)
self.after(1000, updateTotals)
updateTotals()
def getPayment(self) -> None:
# update total payment
Payments.updateTotalPayments(self._pay, self.toPayVar.get())
# update stock values
newData: list[Any] = []
updatedData: list[Any] = []
for item in StockDatabase.loadData(self._db):
item = list(item)
for child in self.shoppingList.get_children():
if self.shoppingList.item(child, 'values')[0] == item[0]:
item[3] -= float(self.shoppingList.item(child, 'values')[1])
updatedData.append(list(item))
# get names of bought products
compareList: list[str] = []
for item in updatedData:
compareList.append(item[0])
print(compareList)
print(updatedData)
# append new data with not bought products
for item in StockDatabase.loadData(self._db):
if item[0] not in compareList:
newData.append(list(item))
# append new data with bought products with new values
for item in updatedData:
newData.append(item)
print(newData)
# write to file procedure
StockDatabase.createNewStockFile(self._db)
for item in newData:
StockDatabase.addNewStockItem(self._db, item)
# clear shopping list after successful purchase
self.shoppingList.delete(*self.shoppingList.get_children())
class StockDatabase:
def __init__(self, dataFile: str) -> None:
self._stockFile = dataFile
# check for stock data file existence
if os.path.isfile(self._stockFile) == True and os.stat(self._stockFile).st_size != 0:
pass
else:
self.createNewStockFile()
def createNewStockFile(self) -> None:
with open(self._stockFile, 'w') as f:
writeData: dict[str, list[Any]] = {"stock": []}
json.dump(writeData, f, indent=4)
def addNewStockItem(self, newItem: list[Any]):
item = {
"item": newItem[0],
"sale price": newItem[1],
"purchase price": newItem[2],
"stock quantity": newItem[3]
}
with open(self._stockFile, 'r') as f:
data = json.load(f)
data["stock"].append(item)
with open(self._stockFile, 'w') as f:
json.dump(data, f, indent=4)
def loadData(self) -> list[Any]:
stockItem = []
with open(self._stockFile, 'r') as f:
data = json.load(f)
for itemData in data["stock"]:
item: str = itemData["item"]
salePrice: float = itemData["sale price"]
purchasePrice: float = itemData["purchase price"]
stockQuantity: float = itemData["stock quantity"]
stockItem.append((item, salePrice, purchasePrice, stockQuantity))
return stockItem
class TillSettings:
def __init__(self, dataFile: str) -> None:
self._settFile = dataFile
# check for settings file existence
if os.path.isfile(self._settFile) == True and os.stat(self._settFile).st_size != 0:
pass
else:
self.createNewSettingsFile()
def createNewSettingsFile(self) -> None:
with open(self._settFile, 'w') as f:
writeData = {"settings" : [{
"sales tax": 0
}]
}
json.dump(writeData, f, indent=4)
def loadSettings(self) -> list[Any]:
settings: list[Any] = []
with open(self._settFile, 'r') as f:
data = json.load(f)
for item in data["settings"]:
settings.append(item)
return settings
class Payments:
def __init__(self, paymentFile: str) -> None:
self._paymentFile = paymentFile
# check for file existence
if os.path.isfile(self._paymentFile) == True and os.stat(self._paymentFile).st_size != 0:
pass
else:
self.createNewPaymentFile()
def createNewPaymentFile(self) -> None:
with open(self._paymentFile, 'w') as f:
writeData = {"Total Payments": 0}
json.dump(writeData, f, indent=4)
def updateTotalPayments(self, payment: float):
with open(self._paymentFile, 'r') as f:
data = json.load(f)
data["Total Payments"] += payment
with open(self._paymentFile, 'w') as f:
json.dump(data, f, indent=4)
if __name__ == '__main__':
_stockFIle = f'{os.path.dirname(__file__)}\\stock_data.json'
_settFile = f'{os.path.dirname(__file__)}\\settings.json'
_paymentFile = f'{os.path.dirname(__file__)}\\payments.json'
db = StockDatabase(_stockFIle)
sett = TillSettings(_settFile)
pay = Payments(_paymentFile)
Application(database= db, settings= sett, payments= pay).mainloop()
1 Answer 1
Violation of PEP8
According to PEP8,
- Imports should be on seperate lines.
- Surround top-level function and class definitions with two blank lines.
- Method definitions inside a class are surrounded by a single blank line.
- Variables and function names should be
snake_case
.
My thoughts
You should create seperate files for GUI and Data Handling part. Currently your code is around 550 lines which makes it hard to navigate over.
I do not know why you have done this
tk.priceVar = ...
. Better make itself.prive_var
.You have justified all the entries to the right. This causes some unwanted behaviour.
Your GUI part is very well done. But I would like some additions -
- Currently one can not remove an item from the basket.
- Your 2nd and 3rd columns are named
#
. Would be better if renamed.
Waiting to see the other parts of the project in action. Happy Coding!
-
\$\begingroup\$ I was always naming my_functions rather than myFunction but been told that way it looks more "proper", will stick to my_function as I prefer it anyway. Yes delete but for shoppingList will be added and some more, just wasn't sure if my approach to build this app right. Need to test adding window again as I didn't have that issue. Splitting the code in to different files would make a lot of sense, will need to do it. Thank you very much for your time. \$\endgroup\$Jakub– Jakub2021年10月15日 07:43:10 +00:00Commented Oct 15, 2021 at 7:43
-
\$\begingroup\$
Remove
button is added now, the issue from your screenshot is also fixed. A few more things and I will re-upload the code. Waiting for one of my friends to bring me a scanner and till draw, so may be able to add barcode reading and a function to open the draw when payment is done. \$\endgroup\$Jakub– Jakub2021年10月15日 21:02:29 +00:00Commented Oct 15, 2021 at 21:02 -
\$\begingroup\$ All the best!!! \$\endgroup\$Random_Pythoneer59– Random_Pythoneer592021年10月17日 06:58:12 +00:00Commented Oct 17, 2021 at 6:58
list[Any]
in line 458 causes error, is it because it's written in old python? \$\endgroup\$list[Any]
is the newer syntax. \$\endgroup\$TypeError: 'type' object is not subscriptable
(python 3.8) \$\endgroup\$str
in place wherefloat
should be, but that is easy to fix. \$\endgroup\$