Skip to main content

How to Build Stock In and Stock Out Functions with CustomTkinter and TinyDB?



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

⬅ Previous Part                                                                                                            ➡ Next Part

Preliminary: 
Before I begin, please activate the virtual environment and install the required dependencies.
python -m venv venv
venv\Scripts\activate
pip install tinydb
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.

Step 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():
	pass

Step 2: Update the login and logout functions
Since 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.

In my extracted toplevel.py file, the login logic is as follows:
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
        ....

Step 4: Import the TinyDB module and update the add and withdraw functions
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")

Step 5: Connect the clear_button with the clear_dashboard function
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:
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)
Bonus
(a) TinyDB vs SQLite

(i) When to Use TinyDB

TinyDB is designed for developer happiness and simplicity. It is an excellent choice if:

  • 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.


Final Wrap-up: 
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
---------------------------------------------------------------------------------------------------------------------------------------------------

🎁 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

-----------------------------------------------------------------------------------------------------------------------------
Need a similar system for your business? 
I build custom Flask web applications and Python automation solutions for SMEs and solopreneurs. 


-----------------------------------------------------------------------------------------------------------------------------

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

Popular Posts

How to Build an Audiobook Workflow Desktop System with Python?

In this tutorial, we will build a simple audiobook player using Python and CustomTkinter. You will learn how to convert text into speech using gTTS and play it with PyGame. We will also implement play, pause, and stop controls, like those in a real audio player. By the end, you will have a clean and functional desktop audiobook app. Prerequisite: This tutorial is part of the standalone tutorial. 📚 View the standalone tutorial Preliminary   Before I begin, it is recommended to activate the virtual environment before installing the relevant dependencies. python -m venv venv venv\Scripts\activate pip install customtkinter pillow gTTS pygame CTkMessagebox pypdf Then, the following steps include setting up the file structure, app.py, and two additional folders: the uploads and media folders. The media folder contains the icons necessary to build the app; there are read, pause, and stop icons, as shown on the diagram. Step 1: Build up the app interface I have 4 sections here: the...

How to Set Up PgAdmin and Adminer Using Docker Compose?

If you are new to database management with Docker, this tutorial will guide you through setting up both PgAdmin and Adminer using Docker containers. By containerising these tools, you can quickly launch lightweight and portable database management environments without installing them directly on your operating system. This approach also makes it easier to manage configurations, updates, and multiple projects across different devices.