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
Preliminary:
Before I begin, please activate the virtual environment and install the required dependencies.
Then, we need to set up the file and folder structure, as below:
All the files and folders remain the same as in the previous tutorial, except that I have added 'Broccoli.png' so I can easily upload a picture in the app. Meanwhile, there's the 'inventory.json,' which is automatically generated once an item is inserted into TinyDB.python -m venv venv
venv\Scripts\activate
pip install tinydbStep 1: Add a button group
Before I begin, I need to create a few buttons; however, in this tutorial, I will focus on the add and withdraw buttons. While the function for the clear button was created in the last tutorial, I need to connect the button widget with the function.Here is my code:
import 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():
passSince this tutorial, I have added a button group; therefore, before login, those buttons need to be disabled and will be back to normal once I have logged into the app.
from 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)
Step 3: Amend the upload_picture function
At the beginning, I will declare the uploaded_filename variable as an empty string, then in the upload_picture function, I will assign the uploaded_filename as a filename, and finally, I will 'globalise' it so both the add_button and withdraw_button functions below can access it. Here is an extracted code:
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.
Here is my code for the add_buttons function in the app.py file:
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")As I have created a clear_dashboard function for logging out of the app. However, now I will connect this function with clear_button without the need to log out.
So, when I click the clear button, it will automatically clear all the input field data. My code is below:
Final Wrap-up:
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)This tutorial demonstrated how to build a simple inventory management workflow using CustomTkinter and TinyDB with dedicated add_button, withdraw_button, and clear_button functionalities. By combining form validation, database operations, and interactive message boxes, the application becomes more user-friendly and efficient for managing stock records. These button functions help streamline adding new items, withdrawing inventory, and resetting input fields, making the overall system cleaner, more organised, and easier to maintain.
Published: May 2026
Last Updated: May 2026
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
Post a Comment