In this tutorial, we will add two important inventory management functions: the Add button and the Withdraw button. These functions allow users to increase or decrease stock quantities directly from the application while keeping the inventory data updated automatically. By implementing these features, the app becomes more practical for tracking item movements, managing stock levels, and improving overall inventory control using Python, CustomTkinter, and TinyDB.
Prerequisite:
This tutorial is part of the CustomTkinter Inventory Management System Series.
📚 View the Complete CustomTkinter Inventory Management System Series
python -m venv venv
venv\Scripts\activate
pip install tinydbimport customtkinter as ctk
# Create a frame to hold all action buttons
# This acts as a button group section
button_frame = ctk.CTkFrame(root, width=100, height=100, fg_color="grey")
button_frame.grid(row=2, column=0, columnspan=5,
padx=10, pady=10, sticky="nsew")
# Make all columns inside the frame expand evenly
for i in range(4):
button_frame.grid_columnconfigure(i, weight=1)
# ---------------- ADD BUTTON ----------------
# Button used to add new inventory records
add_button = ctk.CTkButton(button_frame, text="Add", font=font_bold,
state='disabled', command=add_buttons)
add_button.grid(row=0, column=0, padx=10, pady=10)
# ---------------- CLEAR BUTTON ----------------
# Button used to clear/reset dashboard input fields
clear_button = ctk.CTkButton(button_frame, text="Clear", font=font_bold,
state='disabled',
command=clear_dashboard)
clear_button.grid(row=0, column=1, padx=10, pady=10)
# ---------------- WITHDRAW BUTTON ----------------
# Button used to withdraw or reduce stock quantity
withdraw_button = ctk.CTkButton(button_frame, text="Withdraw", font=font_bold,
state='disabled', command=withdraw_buttons)
withdraw_button.grid(row=0, column=2, padx=10, pady=10)
# ---------------- UPDATE BUTTON ----------------
# Button used to update existing inventory records
update_button = ctk.CTkButton(button_frame, text="Update", font=font_bold,
state='disabled')
update_button.grid(row=0, column=3, padx=10, pady=10)
# ---------------- TABLE BUTTON ----------------
# Button used to display inventory records in table view
table_button = ctk.CTkButton(button_frame, text="Table", font=font_bold,
state='disabled')
table_button.grid(row=0, column=4, padx=10, pady=10)
For the time being, for both the add and withdraw functions, I will ignore them, and I will amend them later on.
def add_buttons():
pass
def withdraw_button():
passfrom CTkMessagebox import CTkMessagebox
def open_login_window(root, log_out_button, item_name_entry, name_entry,
address_entry, email_entry,
stock_status_label, radiobutton_1, radiobutton_2,
quantity_entry, unit_price_entry,
upload_button, update_label,
select_date_label, price_label
add_button, clear_button, withdraw_button,
update_button, table_button
)
# ------------------ LOGIN ------------------
if value == "Log In":
if email in users and users[email] == password:
CTkMessagebox(title="Success",
message="Logged in successfully!",
icon="check")
log_out_button.configure(state='normal')
item_name_entry.configure(state='normal')
name_entry.configure(state='normal')
address_entry.configure(state='normal')
email_entry.configure(state='normal')
stock_status_label.configure(state='normal')
radiobutton_1.configure(state='normal')
radiobutton_2.configure(state='normal')
quantity_entry.configure(state='normal')
unit_price_entry.configure(state='normal')
upload_button.configure(state='normal')
update_label.configure(state='normal')
select_date_label.configure(state='normal')
price_label.configure(state='normal')
######## This part will change #####
# added button group
add_button.configure(state='normal')
clear_button.configure(state='normal')
withdraw_button.configure(state='normal')
update_button.configure(state='normal')
table_button.configure(state='normal')
######## This part will change #####
# Hide the window temporarily
toplevel.withdraw()
return
Meanwhile, in my app.py file, the log-out logic was also updated with those button groups to disable the button functions in the code below.def log_out():
# Open the login window again after logging out
open_login_window(root, log_out_button, item_name_entry, name_entry,
address_entry, email_entry,
stock_status_label, radiobutton_1, radiobutton_2,
quantity_entry, unit_price_entry,
update_label, upload_button,
select_date_label, price_label
add_button, clear_button, withdraw_button,
update_button, table_button)
log_out_button.configure(state='disabled')
item_name_entry.configure(state='disabled')
name_entry.configure(state='disabled')
address_entry.configure(state='disabled')
email_entry.configure(state='disabled')
stock_status_label.configure(state='disabled')
radiobutton_1.configure(state='disabled')
radiobutton_2.configure(state='disabled')
quantity_entry.configure(state='disabled')
unit_price_entry.configure(state='disabled')
update_label.configure(state='disabled')
select_date_label.configure(state='disabled')
price_label.configure(state='disabled')
upload_button.configure(state='disabled')
######## This part will change #####
# Disable dashboard buttons and input fields
# to prevent user interaction after logout
add_button.configure(state='disabled')
clear_button.configure(state='disabled')
withdraw_button.configure(state='disabled')
update_button.configure(state='disabled')
table_button.configure(state='disabled')
######## This part will change #####
# Clear all dashboard fields and reset UI
clear_dashboard()
In order to return to the login window again, I also need to update the login_window logic in the app.py file.
from toplevel import open_login_window
open_login_window(root, log_out_button, item_name_entry,
name_entry, address_entry, email_entry,
stock_status_label, radiobutton_1, radiobutton_2,
quantity_entry, unit_price_entry,
upload_button, update_label,
select_date_label, price_label,
add_button, clear_button, withdraw_button,
update_button, table_button)
uploaded_filename = ""
def upload_picture():
global uploaded_filename
# Open a file dialogue to select image files
file_path = filedialog.askopenfilename(
filetypes=[("Images", "*.png *.jpg *.jpeg")]
)
# Continue only if a file was selected
if file_path:
# Get the file extension (e.g. .png, .jpg
ext = os.path.splitext(file_path)[1]
# Generate a unique filename using UUID
# This prevents duplicate filenames
filename = f"{uuid.uuid4().hex}{ext}"
# Save filename globally
uploaded_filename = filename
....
Now, it's time to amend the add_button and withdraw_button functions in step 1. First, I will retrieve the data that the user has input. Then, I will check whether the input was left blank; if it is left blank, I will prompt an error to alert the user. Otherwise, it will go ahead and store the data in JSON format (inventory/inventory.json). Once the process is completed, it will also inform the users that the data was inserted successfully and clear all the data from every input field.
from tinydb import TinyDB
from CTkMessagebox import CTkMessagebox
db = TinyDB('inventory/inventory.json')
def add_buttons():
# Get values from all input fields
item = item_name_entry.get()
name = name_entry.get()
address = address_entry.get("1.0", "end").strip()
email = email_entry.get()
stock_status = radio_var.get()
quantity = quantity_entry.get()
# Extract the selected date from the label text
select_date = select_date_label.cget("text").replace("Selected date: ", "")
# Get unit price and round it to 2 decimal places
unit_price = round(unit_price_entry.get(), 2)
# Validate that all required fields are filled
if item == "" or name == "" or address == "" or email == "" or \
select_date == "" or \
stock_status == 0 or quantity == "" or unit_price == 0 or \
uploaded_filename == "" or \
select_date == "No date selected." or \
update_label.cget("text") == "No file uploaded yet." or \
price_label.cget("text") == "0.00":
# Show error message if any field is empty
CTkMessagebox(title="Error",
message="Please fill in all fields before adding an item.",
icon="cancel")
return
else:
# Insert the data into TinyDB
db.insert({'type': 'add', 'item': item,
'name': name, 'address': address,
'email': email, 'order_date': order_date,
'stock_status': stock_status, 'quantity': quantity,
'unit_price': unit_price, 'picture': uploaded_filename})
# Clear all dashboard input fields after successful insert
clear_dashboard()
# Show success message
CTkMessagebox(title="Success", message="Item added successfully!",
icon="check")
The withdrawal button is similar to the above, except now I need to compute the availability of the stock; if the withdrawal of an item, either the vegetable or fruit, is more than the availability, it will show an error message.
Here is my code:
from tinydb import TinyDB, Query
from CTkMessagebox import CTkMessagebox
db = TinyDB('inventory/inventory.json')
def withdraw_buttons():
# Get values from all input fields
item = item_name_entry.get()
name = name_entry.get()
address = address_entry.get("1.0", "end").strip()
email = email_entry.get()
stock_status = radio_var.get()
# Since the availability of stock is an integer,
# therefore, quantity is also an integer,
# so, they can be comparable.
quantity = int(quantity_entry.get())
# Extract the selected date from the label text
select_date = select_date_label.cget("text").replace("Selected date: ", "")
# Get unit price and round it to 2 decimal places
unit_price = round(unit_price_entry.get(), 2)
# Get unit price and round it to 2 decimal places
if item == "" or name == "" or address == "" or email == "" or \
select_date == "" or \
stock_status == 0 or quantity == "" or unit_price == 0 or \
uploaded_filename == "" or \
select_date == "No date selected." or \
update_label.cget("text") == "No file uploaded yet." or \
price_label.cget("text") == "0.00":
# Show error message if any field is empty
CTkMessagebox(title="Error",
message="Please fill in all fields before withdrawing an item.",
icon="cancel")
return
else:
# Create a TinyDB query object
Item = Query()
# Find all records where:
# type = "add"
# item = selected item (e.g. Vegetable, Fruit)
add_records = db.search(
(Item.type == "add") & (Item.item == item))
# Find all records where:
# type = "withdraw"
# item = selected item
withdraw_records = db.search(
(Item.type == "withdraw") & (Item.item == item))
# Calculate total quantity added to the inventory
# Example:
# Add 50 Vegetable
# Add 20 Vegetable
# total_added = 70
total_added = sum(
int(record["quantity"]) for record in add_records)
# Calculate total quantity withdrawn from inventory
# Example:
# Withdraw 10 Vegetable
# Withdraw 5 Vegetable
# total_withdrawn = 15
total_withdrawn = sum(
int(record["quantity"]) for record in withdraw_records)
# Current available stock
# Example:
# 70 - 15 = 55
available_stock = total_added - total_withdrawn
# Check whether the requested withdrawal quantity
# exceeds the available stock
if int(quantity) > available_stock:
# Display an error message and prevent the withdrawal
CTkMessagebox(
title="Insufficient Stock",
message=(
f"Available stock for '{item}' is {available_stock}.\n"
f"You requested {quantity}."
),
icon="cancel")
# Exit the function without inserting data into TinyDB
return
# Insert the data into TinyDB
db.insert({'type': 'withdraw', 'item': item,
'name': name, 'address': address,
'email': email, 'order_date': order_date,
'stock_status': stock_status, 'quantity': quantity,
'unit_price': unit_price, 'picture': uploaded_filename})
# Clear all dashboard input fields after successful insert
clear_dashboard()
# Show success message
CTkMessagebox(title="Success",
message="Item withdrawn successfully!",
icon="check")def clear_dashboard():
# Reset item selection dropdown
item_name_entry.set("Select an item")
# Clear text entry fields
name_entry.delete(0, 'end')
address_entry.delete("1.0", "end")
email_entry.delete(0, 'end')
# Reset reorder date and radio button selection
reorder_var.set('')
radio_var.set(0)
# Clear quantity and reset unit price
quantity_entry.delete(0, 'end')
unit_price_entry.set(0)
# Reset upload status label
update_label.configure(text="No file uploaded yet.")
select_date_label.configure(text="No date selected.")
# Restore the default placeholder image
# when the dashboard is cleared
picture_label.configure(
image=default_image,
text="Picture:"
)
# Keep a reference to the default image
# to avoid Tkinter pyimage errors
picture_label.image = default_image
price_label.configure(text="0.00")
# ---------------- CLEAR BUTTON ----------------
# Button used to clear/reset dashboard input fields
clear_button = ctk.CTkButton(button_frame, text="Clear", font=font_bold,
state='disabled',
command=clear_dashboard)
clear_button.grid(row=0, column=1, padx=10, pady=10)(i) When to Use TinyDB
TinyDB is designed for developer happiness and simplicity.
You are prototyping: You need something running in minutes without worrying about table relationships or migrations.
Your data is small: You are working on a simple CLI tool, a personal script, or a small application with a limited dataset.
You prefer Pythonic APIs: You want to interact with data using standard Python dictionaries and lists rather than writing SQL queries.
You need document-oriented storage: Your data is unstructured or nested and doesn't fit neatly into a rigid table structure.
(ii) When to Use SQLite
SQLite is the industry standard for embedded databases. You should choose it if:
Data Integrity is Critical: You need ACID guarantees (atomicity, consistency, isolation, and durability) to ensure data isn't corrupted.
Scale Matters: You expect the dataset to grow, or you need to perform complex queries (JOINs, aggregations, filtering) that would be inefficient in a JSON-based file.
Concurrency is required: Your application might be accessed by multiple threads or processes simultaneously.
Standardisation: You want to use a tool that is supported across almost every programming language and OS, not just Python.
(b) Is TinyDB Replaceable by SQLite?
Yes, technically—but not always practically.
Why you might replace it: As a project grows, you will likely hit the limitations of TinyDB (performance, lack of concurrency, and lack of advanced query capabilities). Switching to SQLite provides a clear path for growth without moving to a client-server architecture like PostgreSQL or MySQL.
Why you might keep it: If the project remains small and "simple," the overhead of managing a relational schema and writing SQL may feel unnecessary.
🎁 Get Your FREE Flask Cheat Sheet
Join 500+ Python learners and receive this printable Flask Cheat Sheet instantly. Plus, get new Flask tutorials delivered to your inbox.
✓ Practical coding tutorials
✓ Automation tips for SMEs
✓ New project ideas and templates
About the Author
Kelvin Loh is a Python developer focused on Flask, desktop applications, and business automation solutions. He shares practical tutorials and real-world coding projects to help developers and small businesses build useful applications.



Comments