Build and Install a Python Inventory App

Build a Python inventory dashboard with SQLite and package it for Windows.

Introduction

30 Second Summary

A shared stock list can look accurate until someone borrows the last item without updating it. The next person only discovers the gap when the shelf is empty.

In this project, you will build InventoryDesk, a modern Windows inventory app for tracking stock from one desktop dashboard. You will save every change in SQLite before packaging the finished app as a Windows installer that runs without a separate Python installation.

What You'll Build

You will launch InventoryDesk from the Start menu to demonstrate a polished dashboard where stock changes survive every restart.

By the end of this project, you'll have:

  • A modern inventory dashboard powered by ttkbootstrap. You can add items through a form. You can review stock in a scrollable table. You can switch between light and dark themes.
  • A persistent inventory workflow backed by SQLite. Your item totals remain after the app closes. Borrowing changes remain too.
  • A shareable Windows installer that launches InventoryDesk without a separate Python installation. PyInstaller bundles the application. Inno Setup provides shortcuts plus uninstall support.
  • Secret Mission: Run a full installation lifecycle test. You will prove inventory data survives uninstalling and reinstalling InventoryDesk. You will then remove the saved data deliberately.

Are there any prerequisites?

You need a Windows computer. Basic Python experience with functions, classes, conditionals, and lists helps you follow the code.

The project guides you through the remaining setup.

Before We Start

Before any hands-on work, define the person or team who needs InventoryDesk. Commit to an installable Windows application that tracks inventory items, total quantities, and borrowed units.

Prepare the Windows Development Environment

InventoryDesk needs a stable foundation before its first window can run. Package conflicts here could make the final Windows executable behave differently from the app you tested.

A project-specific virtual environment keeps the required package versions together. That isolation makes your development build reproducible.

You will confirm Python before creating that environment. You will also prepare the Windows tools that package InventoryDesk.

In this step, get ready to:
  • Confirm a supported Python installation in Visual Studio Code.
  • Create the `inventorydesk` workspace with its selected `.venv` interpreter.
  • Install the pinned packaging tools.
Check Python and prepare the editor

The Windows Python launcher can report which Python 3 version your computer uses. InventoryDesk needs a version newer than 3.10.0.

  • Press the Windows key to open Windows search.
  • Type PowerShell into the search field.
  • Press Enter to open PowerShell.
  • Check your Python 3 version by running this command:
py -3 --version

✔️ Python is newer than 3.10.0

Your Python version supports the packages used by InventoryDesk. Keep PowerShell available while you prepare the editor.

ⓧ Python is 3.10.0 or older

Your installed version is below the project requirement. A current supported Python 3 release gives the project a compatible interpreter.

  • Download a current Python 3 release from the official Python website.
  • Run the downloaded installer.
  • Complete the installer prompts.
  • Close PowerShell after the installation finishes.
  • Reopen PowerShell through Windows search.
  • Check the replacement installation by running:
py -3 --version

You should now see a Python version newer than 3.10.0.

Still seeing the older version?

Close every open PowerShell window before checking again. A fresh terminal picks up the newly installed Python launcher.

If the older version remains, help me check which Python installation the Windows launcher is using.

ⓧ Python is not found

Python needs to be installed before the project environment can exist. The official Windows installer provides the Python 3 launcher used by this project.

  • Download a current Python 3 release from the official Python website.
  • Run the downloaded installer.
  • Complete the installer prompts.
  • Close PowerShell after the installation finishes.
  • Reopen PowerShell through Windows search.
  • Confirm Python is available by running:
py -3 --version

You should see a Python version newer than 3.10.0.

Python still not found?

Restart Windows if a fresh PowerShell window cannot find the launcher. This refreshes the commands available to your account.

If the command still fails, help me troubleshoot why the Windows Python launcher is unavailable.

Visual Studio Code keeps the project folder, terminal, and selected interpreter in one workspace. The Microsoft Python extension connects those editor features to Python.

  • Press the Windows key to open Windows search.
  • Type Visual Studio Code into the search field.
  • Press Enter to try opening Visual Studio Code.

✔️ Visual Studio Code opens

Visual Studio Code is ready. Continue with the extension setup below.

ⓧ Visual Studio Code is not installed

Install the Windows User Installer before continuing. This setup supports updates without requiring administrator access.

  • Download the Windows User Installer from the official Visual Studio Code download page.
  • Run the downloaded installer.
  • Complete the installer prompts.
  • Press the Windows key after installation.
  • Type Visual Studio Code into the search field.
  • Press Enter to open Visual Studio Code.

You should now see the Visual Studio Code window.

Visual Studio Code does not open?

Restart Windows if the newly installed app does not appear in search. This refreshes the Start menu entries.

If it still does not open, help me troubleshoot my Visual Studio Code installation on Windows.

  • Press Ctrl+Shift+X to open the Extensions view.
  • Type Python into the extension search field.
  • Select the Python extension published by Microsoft.
  • Click Install if that button is available.

The extension page shows a management control when installation is complete. Your editor can now discover project-local Python environments.

Opening a local folder may trigger the Workspace Trust dialog. You created this empty folder yourself, so you can enable its workspace features.

  • Click File in the Visual Studio Code menu.
  • Select Open Folder....
  • Choose Desktop in the folder picker.
  • Select New Folder.
  • Name the new folder inventorydesk.
  • Click Select Folder.
  • Select Yes, I trust the authors if Workspace Trust appears.

Good progress. The Explorer now shows inventorydesk as the open workspace.

Cannot see the project folder?

Check the window title for inventorydesk. If another folder is shown, repeat the folder-opening flow from the File menu.

If the folder picker behaves differently, help me open my inventorydesk folder as a Visual Studio Code workspace.

Create an isolated environment

The .venv folder holds an isolated Python interpreter for InventoryDesk. Packages installed there stay separate from other Python projects on your computer.

  • Press Ctrl+` to open the Visual Studio Code integrated terminal.
  • Create the project virtual environment by running this command:
python -m venv .venv

What does this command do?

Python creates a Windows virtual environment inside the .venv folder. Its own interpreter and package directory become the foundation for InventoryDesk.

  • Confirm that .venv appears in the Visual Studio Code Explorer.

The .venv folder did not appear?

Check that the terminal path ends with inventorydesk. The environment must be created from inside that folder.

If Python reports a virtual environment error, help me troubleshoot python -m venv on Windows.

Visual Studio Code uses the selected interpreter for running files and providing Python language features. Selecting the project copy keeps every editor action tied to .venv.

  • Press Ctrl+Shift+P to open the Command Palette.
  • Type Python: Select Interpreter.
  • Press Enter to open the interpreter list.
  • Select the interpreter ending in .venv\Scripts\python.exe.
  • Confirm that the Visual Studio Code Status Bar shows the .venv environment.

That is the isolation in place. Visual Studio Code now runs InventoryDesk with the interpreter stored inside its own project folder.

Install and verify the build tools

ttkbootstrap supplies the themed widgets used by the dashboard. PyInstaller later bundles the Python app into a Windows application folder.

Both packages need pinned versions so the final build matches the environment you test now. The first installation may take a minute while pip downloads their dependencies.

  • Install ttkbootstrap version 2.2.2 with PyInstaller version 6.22.2 by running this command:
.\.venv\Scripts\python.exe -m pip install ttkbootstrap==2.2.2 pyinstaller==6.22.2

What does this command install?

  • The explicit .venv\Scripts\python.exe path sends both packages into the project environment.
  • The version pins keep the interface package at 2.2.2.
  • The packaging tool stays at 6.22.2.

The terminal finishes with successful installation output. Those packages now belong to the inventorydesk environment.

Package installation failed?

Check that the command starts with the .venv interpreter path. A missing .venv folder means the previous environment command did not finish.

If the download or installation still fails, help me troubleshoot the pinned ttkbootstrap and PyInstaller installation.

Inno Setup turns the bundled application folder into a standard Windows installer. Version 7.1.0 provides the 64-bit compiler used later in the project.

  • Check the installed Inno Setup 7 package by running this command:
winget list --id JRSoftware.InnoSetup.7 -e

✔️ I see version 7.1.0

Your 64-bit Inno Setup installation matches the project version. Continue to the final toolchain check below.

ⓧ I see an older version

The installed compiler needs an upgrade before you create the InventoryDesk installer.

  • Upgrade Inno Setup 7 by running this command:
winget upgrade --id JRSoftware.InnoSetup.7

Windows Package Manager replaces the older release with the current Inno Setup 7 package. Run the package check again after it finishes.

ⓧ Inno Setup is not listed

The vendor installation command opens an interactive installer for the 64-bit edition. Follow its prompts to complete the setup.

  • Install Inno Setup 7 by running this command:
winget install --id JRSoftware.InnoSetup.7 -e -s winget -i

The interactive installer adds Inno Setup 7 to Windows. Run the package check again after the installer closes.

Winget cannot complete the install?

Use the official Inno Setup download page to download the 64-bit installer. Run that installer to complete the same setup.

If the installation still fails, help me troubleshoot installing Inno Setup 7 on Windows.

  • Return to Visual Studio Code.

Before you run the final check, do you expect every tool to report the pinned version from this setup?

  • Verify the complete Python toolchain by running these commands:
py -3 --version
.\.venv\Scripts\ttkb.exe version
.\.venv\Scripts\pyinstaller.exe --version

What should I see?

  • The Python command reports a version newer than 3.10.0.
  • The ttkbootstrap command reports 2.2.2.
  • The PyInstaller command reports 6.22.2.
  • Press the Windows key to open Windows search.
  • Type Inno Setup Compiler into the search field.
  • Press Enter to open Inno Setup Compiler.

Your terminal now confirms all three required versions. The open compiler confirms that the Windows installer tool is available.

Strong start. InventoryDesk now has an isolated Python environment, a configured editor, and the complete Windows packaging toolchain.

Your development environment is ready. Next up, you will build the modern InventoryDesk dashboard shell and see it running on Windows.

Build the Modern Dashboard Shell

InventoryDesk needs a clear dashboard before its inventory rules can feel useful. This step turns the workflow you planned into a desktop window that responds to clicks.

You will use ttkbootstrap to build the themed interface. You will finish by testing how the first Add Item workflow behaves after a restart.

In this step, get ready to:
  • Build a resizable dashboard with a themed header and summary cards.
  • Add an inventory form with a scrollable table and stock controls.
  • Test a session-only inventory row after restarting the application.
Create the dashboard structure

An event-driven interface waits for actions such as button clicks or window resizing. The application window owns every widget that responds to those events.

  • Click the New File icon in the Visual Studio Code Explorer sidebar.
  • Enter app.py as the file name.
  • Create the themed application window by adding this code to app.py:
import ttkbootstrap as ttk

APP_NAME = "InventoryDesk"


class InventoryApp:
    def __init__(self):
        # Create the window that owns every dashboard widget.
        self.root = ttk.App(
            title=APP_NAME,
            theme="bootstrap-light",
            size=(1080, 680),
            minsize=(900, 600),
        )
        self.root.set_theme_modes(
            light="bootstrap-light",
            dark="bootstrap-dark",
        )
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(2, weight=1)

    def run(self):
        # Keep the interface listening for user actions.
        self.root.mainloop()


if __name__ == "__main__":
    InventoryApp().run()

What does this code do?

  • The ttk.App object creates the main InventoryDesk window.
  • The theme modes give the window a light theme plus a dark theme.
  • The grid weights reserve an expandable content row for the dashboard.
  • The mainloop() method keeps the interface responsive to Windows events.
  • Save app.py.
  • Run the first window from the existing VS Code terminal by using this command:
.\.venv\Scripts\python.exe app.py

What should you see?

You should see an empty window titled InventoryDesk. Resizing the window confirms that the application is running as a desktop interface.

  • Close the InventoryDesk window.

Window not opening?

Confirm that the terminal is inside the inventorydesk folder containing app.py.

Check that every method inside InventoryApp uses consistent indentation.

Share the terminal output to get help diagnosing why the InventoryDesk window does not open.

Shared text variables let widgets display values that change while the application is running. Fonts provide a consistent visual hierarchy across the header and summary cards.

  • In __init__(), add these values directly below self.root.rowconfigure(2, weight=1):
        # Store fonts and text values shared by the interface.
        self.title_font = ttk.Font(root=self.root, size=22, weight="bold")
        self.metric_font = ttk.Font(root=self.root, size=19, weight="bold")

        self.name_var = ttk.StringVar(master=self.root)
        self.quantity_var = ttk.StringVar(master=self.root, value="1")
        self.total_items_var = ttk.StringVar(master=self.root, value="0")
        self.total_units_var = ttk.StringVar(master=self.root, value="0")
        self.borrowed_units_var = ttk.StringVar(master=self.root, value="0")
        self.status_var = ttk.StringVar(master=self.root, value="Ready")

        self.build_interface()

Why use shared variables?

Each StringVar stores text for one or more widgets. Changing the stored value updates the connected widget without rebuilding the interface.

  • Add the build_interface() method above run() by using this code:
    def build_interface(self):
        # Give the dashboard a title and a theme control.
        header = ttk.Frame(self.root, padding=(24, 20, 24, 10))
        header.grid(row=0, column=0, sticky="ew")
        header.columnconfigure(0, weight=1)

        ttk.Label(
            header,
            text=APP_NAME,
            font=self.title_font,
            bootstyle="primary",
        ).grid(row=0, column=0, sticky="w")

        ttk.Label(
            header,
            text="A focused inventory and lending dashboard",
            bootstyle="secondary",
        ).grid(row=1, column=0, sticky="w", pady=(4, 0))

        ttk.Button(
            header,
            text="Toggle Theme",
            command=self.root.toggle_theme,
            bootstyle="primary outline",
        ).grid(row=0, column=1, rowspan=2, sticky="e")

How does the header work?

  • The expandable first column keeps the title aligned left.
  • The theme button stays aligned to the right edge of the header.
  • The semantic bootstyle values let the active theme choose readable colours.
  • Save app.py.
  • Run the dashboard with its new header by using this command:
.\.venv\Scripts\python.exe app.py

Check the themed header

You should see the InventoryDesk title beside a Toggle Theme button. Clicking the button should switch the entire window between light and dark themes.

  • Close the InventoryDesk window.

Header missing?

Confirm that self.build_interface() is indented inside __init__().

Confirm that build_interface() aligns with run() inside the class.

Use the current file to get help fixing a missing InventoryDesk header.

Summary cards turn inventory totals into a quick status check. Equal grid weights keep all three cards the same width when the window grows.

  • In build_interface(), add the summary cards directly below the header button:
        # Present the inventory totals as equal-width cards.
        cards = ttk.Frame(self.root, padding=(24, 4, 24, 12))
        cards.grid(row=1, column=0, sticky="ew")
        for column in range(3):
            cards.columnconfigure(column, weight=1)

        card_data = (
            ("Item Types", self.total_items_var, "primary"),
            ("Total Units", self.total_units_var, "success"),
            ("Borrowed", self.borrowed_units_var, "warning"),
        )
        for column, (title, variable, style) in enumerate(card_data):
            card = ttk.Labelframe(cards, text=title, padding=14)
            card.grid(
                row=0,
                column=column,
                sticky="ew",
                padx=(0 if column == 0 else 6, 0 if column == 2 else 6),
            )
            ttk.Label(
                card,
                textvariable=variable,
                font=self.metric_font,
                bootstyle=style,
            ).pack(anchor="w")

How do the cards stay responsive?

Each card receives the same grid weight. The frame shares extra width evenly across the three inventory totals.

The labels read from the shared variables created earlier. Those values currently start at 0.

  • Save app.py.
  • Run the dashboard to check the summary area by using this command:
.\.venv\Scripts\python.exe app.py

Check the summary cards

You should see three equal-width cards labelled Item Types, Total Units, and Borrowed. Each card should display 0.

  • Resize the window horizontally.
  • Close the InventoryDesk window after confirming the cards expand evenly.
Add the inventory controls

The dashboard needs one area for entering inventory plus another area for viewing it. A Treeview presents each item as a selectable row with consistent columns.

  • In build_interface(), add the resizable content area directly below the summary card loop:
        # Split the main area between the form and the table.
        content = ttk.Frame(self.root, padding=(24, 0, 24, 12))
        content.grid(row=2, column=0, sticky="nsew")
        content.columnconfigure(1, weight=1)
        content.rowconfigure(0, weight=1)

        form = ttk.Labelframe(content, text="Add inventory", padding=16)
        form.grid(row=0, column=0, sticky="ns", padx=(0, 8))
        form.columnconfigure(0, weight=1)

Why split the content area?

The form keeps a stable width for predictable data entry. The weighted table column receives the remaining space.

  • Add the item fields directly below the form grid configuration:
        # Collect the item name and starting quantity.
        ttk.Label(form, text="Item name").grid(row=0, column=0, sticky="w")
        ttk.Entry(
            form,
            textvariable=self.name_var,
            width=28,
            bootstyle="primary",
        ).grid(row=1, column=0, sticky="ew", pady=(4, 14))

        ttk.Label(form, text="Total quantity").grid(row=2, column=0, sticky="w")
        ttk.Entry(
            form,
            textvariable=self.quantity_var,
            width=28,
            bootstyle="primary",
        ).grid(row=3, column=0, sticky="ew", pady=(4, 14))

        ttk.Button(
            form,
            text="Add Item",
            bootstyle="primary",
        ).grid(row=4, column=0, sticky="ew")

        ttk.Label(
            form,
            text="Select a row before borrowing or returning one unit.",
            wraplength=220,
            bootstyle="secondary",
        ).grid(row=5, column=0, sticky="w", pady=(18, 0))

How does the form store input?

The two entry fields write into name_var and quantity_var. The quantity begins at 1 to match the smallest useful inventory amount.

The button is visible now. Its click workflow is connected in the final substep.

  • Save app.py.
  • Run the dashboard to inspect the form by using this command:
.\.venv\Scripts\python.exe app.py

Check the inventory form

You should see an Add inventory panel below the summary cards. The panel should contain two fields plus an Add Item button.

  • Close the InventoryDesk window.

Form not visible?

Confirm that the content frame uses row 2 of the application grid.

Check that the form widgets stay indented inside build_interface().

Use the current layout code to get help fixing an InventoryDesk form that does not appear.

  • Add the table panel directly below the form helper label:
        # Create a resizable table for the inventory rows.
        table_panel = ttk.Labelframe(content, text="Inventory", padding=12)
        table_panel.grid(row=0, column=1, sticky="nsew", padx=(8, 0))
        table_panel.columnconfigure(0, weight=1)
        table_panel.rowconfigure(0, weight=1)

        columns = ("name", "quantity", "borrowed", "available", "status")
        self.tree = ttk.Treeview(
            table_panel,
            columns=columns,
            show="headings",
            selectmode="browse",
            height=15,
            bootstyle="primary",
        )
        self.tree.grid(row=0, column=0, sticky="nsew")

What does the table represent?

Each row represents one inventory item. The five columns separate its name from the stock totals that change over time.

The browse selection mode keeps one row selected at a time. That gives later stock actions one clear target.

  • Add the table headings directly below the Treeview grid call:
        # Give each stored value a readable column heading.
        headings = {
            "name": "Item",
            "quantity": "Total",
            "borrowed": "Borrowed",
            "available": "Available",
            "status": "Status",
        }
        widths = {
            "name": 220,
            "quantity": 80,
            "borrowed": 90,
            "available": 90,
            "status": 110,
        }
        for column in columns:
            self.tree.heading(column, text=headings[column])
            self.tree.column(
                column,
                width=widths[column],
                anchor="w" if column == "name" else "center",
            )

Why configure each column?

The dictionaries keep labels separate from layout values. The item name aligns left for scanning while the numeric stock values stay centred.

  • Save app.py.
  • Run the dashboard to check the table by using this command:
.\.venv\Scripts\python.exe app.py

Check the inventory table

You should see a large Inventory panel beside the form. Its headings should read Item, Total, Borrowed, Available, and Status.

  • Resize the InventoryDesk window.
  • Close the window after confirming that the table receives the extra space.

Table columns missing?

Confirm that every name in columns has a matching entry in both dictionaries.

Check that the heading loop remains indented inside build_interface().

Use the table code to get help fixing missing InventoryDesk columns.

A scrollbar keeps long inventories usable within one window. The stock buttons establish where the borrowing workflow belongs once its rules are added.

  • Add the scrollbar and stock controls directly below the column loop:
        # Keep long inventories scrollable within the table panel.
        scrollbar = ttk.Scrollbar(
            table_panel,
            orient="vertical",
            command=self.tree.yview,
        )
        scrollbar.grid(row=0, column=1, sticky="ns")
        self.tree.configure(yscrollcommand=scrollbar.set)

        # Reserve the stock controls below the selected row.
        actions = ttk.Frame(table_panel, padding=(0, 12, 0, 0))
        actions.grid(row=1, column=0, columnspan=2, sticky="ew")
        actions.columnconfigure((0, 1), weight=1)

        ttk.Button(
            actions,
            text="Borrow One",
            bootstyle="warning",
        ).grid(row=0, column=0, sticky="ew", padx=(0, 6))

        ttk.Button(
            actions,
            text="Return One",
            bootstyle="success",
        ).grid(row=0, column=1, sticky="ew", padx=(6, 0))

How do these controls fit together?

The scrollbar follows the table's vertical position. The two stock buttons share the action row evenly.

The stock buttons establish the completed dashboard layout. Their inventory rules arrive after persistent storage is in place.

  • Add the status label directly below the Return One button:
        # Show immediate feedback along the bottom of the window.
        ttk.Label(
            self.root,
            textvariable=self.status_var,
            padding=(24, 8, 24, 16),
            bootstyle="info",
        ).grid(row=3, column=0, sticky="ew")

Why add a status line?

The status line gives feedback inside the dashboard. It keeps validation messages close to the workflow without opening extra windows.

  • Save app.py.
  • Run the complete dashboard layout by using this command:
.\.venv\Scripts\python.exe app.py

Check the stock controls

You should see Borrow One plus Return One below the table. A status line showing Ready should span the bottom of the window.

  • Close the InventoryDesk window.

Controls outside the table?

Confirm that actions uses table_panel as its parent.

Confirm that the status label uses self.root as its parent.

Use the layout code to get help correcting the InventoryDesk control placement.

Experience the missing persistence

The dashboard can now collect a name plus a quantity. Its first callback inserts that information into the visible table while the program is running.

  • Add the add_item() method above run() by using this code:
    def add_item(self):
        # Read and validate the values entered in the form.
        name = self.name_var.get().strip()
        try:
            quantity = int(self.quantity_var.get())
        except ValueError:
            self.status_var.set("Enter a whole number for quantity.")
            return

        if not name:
            self.status_var.set("Enter an item name.")
            return
        if quantity <= 0:
            self.status_var.set("Quantity must be greater than zero.")
            return

        # Insert one row into the current dashboard session.
        item_id = int(self.total_items_var.get()) + 1
        self.tree.insert(
            "",
            "end",
            iid=str(item_id),
            values=(name, quantity, 0, quantity, "Available"),
        )
        self.total_items_var.set(str(item_id))
        self.total_units_var.set(str(int(self.total_units_var.get()) + quantity))
        self.name_var.set("")
        self.quantity_var.set("1")
        self.status_var.set(f"Added {name}.")

What does the Add Item workflow do?

  • The callback rejects an empty item name.
  • The callback rejects quantities that are not positive whole numbers.
  • A valid item becomes a table row with all units available.
  • The summary cards update immediately after a valid insertion.
  • In build_interface(), find the current Add Item button:
        ttk.Button(
            form,
            text="Add Item",
            bootstyle="primary",
        ).grid(row=4, column=0, sticky="ew")

What is missing from this button?

The current button has its label plus its theme style. It needs a callback before a click can insert a row.

  • Replace the current Add Item button with this connected version:
        ttk.Button(
            form,
            text="Add Item",
            command=self.add_item,
            bootstyle="primary",
        ).grid(row=4, column=0, sticky="ew")

How is the click connected?

The command option passes each button click to add_item(). The callback reads the latest form values at that moment.

Use this comparison if you want to confirm that every dashboard section is in the correct place.

✔️ Awesome, I've got everything!

Great. Save app.py before testing the completed dashboard.

ⓧ I'd like to double check the full code

import ttkbootstrap as ttk

APP_NAME = "InventoryDesk"


class InventoryApp:
    def __init__(self):
        # Create the window that owns every dashboard widget.
        self.root = ttk.App(
            title=APP_NAME,
            theme="bootstrap-light",
            size=(1080, 680),
            minsize=(900, 600),
        )
        self.root.set_theme_modes(
            light="bootstrap-light",
            dark="bootstrap-dark",
        )
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(2, weight=1)

        # Store fonts and text values shared by the interface.
        self.title_font = ttk.Font(root=self.root, size=22, weight="bold")
        self.metric_font = ttk.Font(root=self.root, size=19, weight="bold")

        self.name_var = ttk.StringVar(master=self.root)
        self.quantity_var = ttk.StringVar(master=self.root, value="1")
        self.total_items_var = ttk.StringVar(master=self.root, value="0")
        self.total_units_var = ttk.StringVar(master=self.root, value="0")
        self.borrowed_units_var = ttk.StringVar(master=self.root, value="0")
        self.status_var = ttk.StringVar(master=self.root, value="Ready")

        self.build_interface()

    def build_interface(self):
        # Give the dashboard a title and a theme control.
        header = ttk.Frame(self.root, padding=(24, 20, 24, 10))
        header.grid(row=0, column=0, sticky="ew")
        header.columnconfigure(0, weight=1)

        ttk.Label(
            header,
            text=APP_NAME,
            font=self.title_font,
            bootstyle="primary",
        ).grid(row=0, column=0, sticky="w")

        ttk.Label(
            header,
            text="A focused inventory and lending dashboard",
            bootstyle="secondary",
        ).grid(row=1, column=0, sticky="w", pady=(4, 0))

        ttk.Button(
            header,
            text="Toggle Theme",
            command=self.root.toggle_theme,
            bootstyle="primary outline",
        ).grid(row=0, column=1, rowspan=2, sticky="e")

        # Present the inventory totals as equal-width cards.
        cards = ttk.Frame(self.root, padding=(24, 4, 24, 12))
        cards.grid(row=1, column=0, sticky="ew")
        for column in range(3):
            cards.columnconfigure(column, weight=1)

        card_data = (
            ("Item Types", self.total_items_var, "primary"),
            ("Total Units", self.total_units_var, "success"),
            ("Borrowed", self.borrowed_units_var, "warning"),
        )
        for column, (title, variable, style) in enumerate(card_data):
            card = ttk.Labelframe(cards, text=title, padding=14)
            card.grid(
                row=0,
                column=column,
                sticky="ew",
                padx=(0 if column == 0 else 6, 0 if column == 2 else 6),
            )
            ttk.Label(
                card,
                textvariable=variable,
                font=self.metric_font,
                bootstyle=style,
            ).pack(anchor="w")

        # Split the main area between the form and the table.
        content = ttk.Frame(self.root, padding=(24, 0, 24, 12))
        content.grid(row=2, column=0, sticky="nsew")
        content.columnconfigure(1, weight=1)
        content.rowconfigure(0, weight=1)

        form = ttk.Labelframe(content, text="Add inventory", padding=16)
        form.grid(row=0, column=0, sticky="ns", padx=(0, 8))
        form.columnconfigure(0, weight=1)

        # Collect the item name and starting quantity.
        ttk.Label(form, text="Item name").grid(row=0, column=0, sticky="w")
        ttk.Entry(
            form,
            textvariable=self.name_var,
            width=28,
            bootstyle="primary",
        ).grid(row=1, column=0, sticky="ew", pady=(4, 14))

        ttk.Label(form, text="Total quantity").grid(row=2, column=0, sticky="w")
        ttk.Entry(
            form,
            textvariable=self.quantity_var,
            width=28,
            bootstyle="primary",
        ).grid(row=3, column=0, sticky="ew", pady=(4, 14))

        ttk.Button(
            form,
            text="Add Item",
            command=self.add_item,
            bootstyle="primary",
        ).grid(row=4, column=0, sticky="ew")

        ttk.Label(
            form,
            text="Select a row before borrowing or returning one unit.",
            wraplength=220,
            bootstyle="secondary",
        ).grid(row=5, column=0, sticky="w", pady=(18, 0))

        # Create a resizable table for the inventory rows.
        table_panel = ttk.Labelframe(content, text="Inventory", padding=12)
        table_panel.grid(row=0, column=1, sticky="nsew", padx=(8, 0))
        table_panel.columnconfigure(0, weight=1)
        table_panel.rowconfigure(0, weight=1)

        columns = ("name", "quantity", "borrowed", "available", "status")
        self.tree = ttk.Treeview(
            table_panel,
            columns=columns,
            show="headings",
            selectmode="browse",
            height=15,
            bootstyle="primary",
        )
        self.tree.grid(row=0, column=0, sticky="nsew")

        # Give each stored value a readable column heading.
        headings = {
            "name": "Item",
            "quantity": "Total",
            "borrowed": "Borrowed",
            "available": "Available",
            "status": "Status",
        }
        widths = {
            "name": 220,
            "quantity": 80,
            "borrowed": 90,
            "available": 90,
            "status": 110,
        }
        for column in columns:
            self.tree.heading(column, text=headings[column])
            self.tree.column(
                column,
                width=widths[column],
                anchor="w" if column == "name" else "center",
            )

        # Keep long inventories scrollable within the table panel.
        scrollbar = ttk.Scrollbar(
            table_panel,
            orient="vertical",
            command=self.tree.yview,
        )
        scrollbar.grid(row=0, column=1, sticky="ns")
        self.tree.configure(yscrollcommand=scrollbar.set)

        # Reserve the stock controls below the selected row.
        actions = ttk.Frame(table_panel, padding=(0, 12, 0, 0))
        actions.grid(row=1, column=0, columnspan=2, sticky="ew")
        actions.columnconfigure((0, 1), weight=1)

        ttk.Button(
            actions,
            text="Borrow One",
            bootstyle="warning",
        ).grid(row=0, column=0, sticky="ew", padx=(0, 6))

        ttk.Button(
            actions,
            text="Return One",
            bootstyle="success",
        ).grid(row=0, column=1, sticky="ew", padx=(6, 0))

        # Show immediate feedback along the bottom of the window.
        ttk.Label(
            self.root,
            textvariable=self.status_var,
            padding=(24, 8, 24, 16),
            bootstyle="info",
        ).grid(row=3, column=0, sticky="ew")

    def add_item(self):
        # Read and validate the values entered in the form.
        name = self.name_var.get().strip()
        try:
            quantity = int(self.quantity_var.get())
        except ValueError:
            self.status_var.set("Enter a whole number for quantity.")
            return

        if not name:
            self.status_var.set("Enter an item name.")
            return
        if quantity <= 0:
            self.status_var.set("Quantity must be greater than zero.")
            return

        # Insert one row into the current dashboard session.
        item_id = int(self.total_items_var.get()) + 1
        self.tree.insert(
            "",
            "end",
            iid=str(item_id),
            values=(name, quantity, 0, quantity, "Available"),
        )
        self.total_items_var.set(str(item_id))
        self.total_units_var.set(str(int(self.total_units_var.get()) + quantity))
        self.name_var.set("")
        self.quantity_var.set("1")
        self.status_var.set(f"Added {name}.")

    def run(self):
        # Keep the interface listening for user actions.
        self.root.mainloop()


if __name__ == "__main__":
    InventoryApp().run()
  • Save app.py.
  • Run the connected dashboard by using this command:
.\.venv\Scripts\python.exe app.py

Test the Add Item workflow

The dashboard is ready for one complete interaction. A valid item should appear as a new row while its totals update above the table.

  • Enter HDMI Cable in the Item name field.
  • Enter 3 in the Total quantity field.
  • Click Add Item.

You should see an HDMI Cable row with 3 available units. The summary cards should show one item type plus three total units.

  • Close the InventoryDesk window.

Before you restart InventoryDesk, do you expect the row to return? Hold that prediction while you run the check.

  • Restart the dashboard by running this command:
.\.venv\Scripts\python.exe app.py

The row has disappeared

You should see the polished dashboard again without the HDMI Cable row. The summary values should also return to 0.

This shortfall is intentional. The callback placed the row only inside the running interface, so closing the application removed its inventory state.

Row did not appear before restarting?

Confirm that the Add Item button includes command=self.add_item.

Check the bottom status line for feedback about the item name or quantity.

Use the current values plus the status message to get help debugging why Add Item does not insert a row.

You have a responsive inventory dashboard plus a visible reason to strengthen it. Next, you will give every item a durable home that survives closing the application.

Persist Inventory with SQLite

Your dashboard is running with a polished inventory workflow. The restart test exposed the weak point because every added row disappeared when the process ended.

A SQLite database gives those rows a durable home on the learner's computer. This step connects the dashboard to that database so every restart restores the saved inventory.

In this step, get ready to:
  • Create durable local storage for inventory records.
  • Replace the session-only Add Item workflow with validated database writes.
  • Prove that inventory rows survive an application restart.
Create durable local storage

The database belongs in the current user's home directory. This location keeps inventory data separate from the application files.

  • In app.py, add these imports above the existing ttkbootstrap import:
import sqlite3
from pathlib import Path

What do these imports provide?

  • The sqlite3 module creates the database connection and runs SQL statements.
  • The Path class builds a user-specific database path without hard-coding a Windows username.
  • Below APP_NAME, add the data directory and database paths:
DATA_DIR = Path.home() / ".inventorydesk"
DB_PATH = DATA_DIR / "inventory.db"

Where will the data live?

The DATA_DIR path points to a hidden .inventorydesk folder under the current user's home directory. The DB_PATH value places inventory.db inside that folder.

  • Locate def build_interface(self): in app.py.
  • Insert this database creation method directly above it:
    def create_database(self):
        self.connection.execute(
            """
            CREATE TABLE IF NOT EXISTS inventory (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL UNIQUE COLLATE NOCASE,
                quantity INTEGER NOT NULL CHECK(quantity >= 0),
                borrowed INTEGER NOT NULL DEFAULT 0 CHECK(borrowed >= 0),
                CHECK(borrowed <= quantity)
            )
            """
        )
        self.connection.commit()

What does the inventory table protect?

  • The id column gives every row a stable identifier.
  • The unique case-insensitive name column prevents duplicate item names.
  • The quantity checks prevent negative values and stop borrowed stock from exceeding total stock.
  • Add this seeding method directly below create_database():
    def seed_database(self):
        item_count = self.connection.execute(
            "SELECT COUNT(*) FROM inventory"
        ).fetchone()[0]
        if item_count == 0:
            with self.connection:
                self.connection.execute(
                    "INSERT INTO inventory (name, quantity, borrowed) VALUES (?, ?, ?)",
                    ("USB-C Dock", 6, 1),
                )

Why seed one item?

The count query checks whether the table is empty. A new database receives one USB-C Dock row so the dashboard has an understandable example on its first launch.

Later launches skip the insert because the table already contains data. Your saved rows remain untouched.

  • At the start of __init__(), add these lines before the existing self.root = ttk.App( line:
        DATA_DIR.mkdir(parents=True, exist_ok=True)
        self.connection = sqlite3.connect(DB_PATH)
        self.create_database()
        self.seed_database()

How does startup prepare the database?

  • The directory call creates .inventorydesk when it is missing.
  • The connection opens inventory.db or creates it on the first launch.
  • The final two calls prepare the table and seed its first row.

Having trouble starting the database?

Check that create_database() and seed_database() align with the other methods inside InventoryApp. Incorrect indentation can place a method inside __init__().

Confirm the connection line appears before either database method is called.

help me fix the database startup code in my InventoryApp class

Replace session-only inventory writes

The current Add Item callback writes directly to the visible table. The new callback validates each value before sending it to SQLite with placeholders.

  • Replace the existing add_item() method with this database-backed version:
    def add_item(self):
        name = self.name_var.get().strip()
        try:
            quantity = int(self.quantity_var.get())
        except ValueError:
            self.set_status("Enter a whole number for quantity.")
            return

        if not name:
            self.set_status("Enter an item name.")
            return
        if quantity <= 0:
            self.set_status("Quantity must be greater than zero.")
            return

        try:
            with self.connection:
                self.connection.execute(
                    "INSERT INTO inventory (name, quantity, borrowed) VALUES (?, ?, 0)",
                    (name, quantity),
                )
        except sqlite3.IntegrityError:
            self.set_status(f"{name} already exists.")
            return

        self.name_var.set("")
        self.quantity_var.set("1")
        self.refresh_inventory()
        self.set_status(f"Added {name}.")

What does the new Add Item workflow do?

  • The conversion rejects quantity values that are not whole numbers.
  • The validation checks reject blank names and quantities below one.
  • The SQL placeholders keep typed values separate from the statement.
  • The sqlite3.IntegrityError handler turns a duplicate name into an inline status message.
  • Locate def set_status(self, message): near the bottom of app.py.
  • Insert this database-backed refresh_inventory() method directly above it:
    def refresh_inventory(self):
        for item in self.tree.get_children():
            self.tree.delete(item)

        rows = self.connection.execute(
            "SELECT id, name, quantity, borrowed "
            "FROM inventory ORDER BY name COLLATE NOCASE"
        ).fetchall()

        total_units = 0
        borrowed_units = 0
        for item_id, name, quantity, borrowed in rows:
            available = quantity - borrowed
            status = "Available" if available > 0 else "Out of stock"
            self.tree.insert(
                "",
                "end",
                iid=str(item_id),
                values=(name, quantity, borrowed, available, status),
            )
            total_units += quantity
            borrowed_units += borrowed

        self.total_items_var.set(str(len(rows)))
        self.total_units_var.set(str(total_units))
        self.borrowed_units_var.set(str(borrowed_units))

How does the dashboard rebuild itself?

  • The method clears the visible rows before loading the current database state.
  • The sorted query returns every inventory record by case-insensitive item name.
  • Each loop calculates available units and the stock status shown in the table.
  • The final values update all three summary cards from the same database rows.
  • At the end of __init__(), replace the existing interface startup lines with these lines:
        self.build_interface()
        self.refresh_inventory()
        self.root.on_close(self.close_database)

Why register a close callback?

The refresh call loads saved inventory after the widgets exist. The close callback gives the application a dedicated place to close its database connection.

  • Locate def run(self): near the bottom of app.py.
  • Insert this cleanup method directly above it:
    def close_database(self):
        self.connection.close()

What happens when InventoryDesk closes?

The close callback releases the active SQLite connection before the window exits. Each committed inventory change remains stored in inventory.db.

  • Save app.py.

Seeing a database or refresh problem?

Check that the old session-only table insertion has been removed from add_item(). The callback should insert through self.connection.

Confirm refresh_inventory() is a class method with the same indentation as add_item().

help me debug my SQLite-backed add and refresh methods

Use the tabs below to confirm that every persistence change is present before testing the restart.

✔️ Awesome, I've got everything!

Your persistence code is ready. Double-check that you saved app.py before launching the application.

ⓧ I'd like to double check the full code

import sqlite3
from pathlib import Path

import ttkbootstrap as ttk


APP_NAME = "InventoryDesk"
DATA_DIR = Path.home() / ".inventorydesk"
DB_PATH = DATA_DIR / "inventory.db"


class InventoryApp:
    def __init__(self):
        DATA_DIR.mkdir(parents=True, exist_ok=True)
        self.connection = sqlite3.connect(DB_PATH)
        self.create_database()
        self.seed_database()

        self.root = ttk.App(
            title=APP_NAME,
            theme="bootstrap-light",
            size=(1080, 680),
            minsize=(900, 600),
        )
        self.root.set_theme_modes(
            light="bootstrap-light",
            dark="bootstrap-dark",
        )
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(2, weight=1)

        self.title_font = ttk.Font(root=self.root, size=22, weight="bold")
        self.metric_font = ttk.Font(root=self.root, size=19, weight="bold")

        self.name_var = ttk.StringVar(master=self.root)
        self.quantity_var = ttk.StringVar(master=self.root, value="1")
        self.total_items_var = ttk.StringVar(master=self.root, value="0")
        self.total_units_var = ttk.StringVar(master=self.root, value="0")
        self.borrowed_units_var = ttk.StringVar(master=self.root, value="0")
        self.status_var = ttk.StringVar(master=self.root, value="Ready")

        self.build_interface()
        self.refresh_inventory()
        self.root.on_close(self.close_database)

    def create_database(self):
        self.connection.execute(
            """
            CREATE TABLE IF NOT EXISTS inventory (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL UNIQUE COLLATE NOCASE,
                quantity INTEGER NOT NULL CHECK(quantity >= 0),
                borrowed INTEGER NOT NULL DEFAULT 0 CHECK(borrowed >= 0),
                CHECK(borrowed <= quantity)
            )
            """
        )
        self.connection.commit()

    def seed_database(self):
        item_count = self.connection.execute(
            "SELECT COUNT(*) FROM inventory"
        ).fetchone()[0]
        if item_count == 0:
            with self.connection:
                self.connection.execute(
                    "INSERT INTO inventory (name, quantity, borrowed) VALUES (?, ?, ?)",
                    ("USB-C Dock", 6, 1),
                )

    def build_interface(self):
        header = ttk.Frame(self.root, padding=(24, 20, 24, 10))
        header.grid(row=0, column=0, sticky="ew")
        header.columnconfigure(0, weight=1)

        ttk.Label(
            header,
            text=APP_NAME,
            font=self.title_font,
            bootstyle="primary",
        ).grid(row=0, column=0, sticky="w")

        ttk.Label(
            header,
            text="A focused inventory and lending dashboard",
            bootstyle="secondary",
        ).grid(row=1, column=0, sticky="w", pady=(4, 0))

        ttk.Button(
            header,
            text="Toggle Theme",
            command=self.root.toggle_theme,
            bootstyle="primary outline",
        ).grid(row=0, column=1, rowspan=2, sticky="e")

        cards = ttk.Frame(self.root, padding=(24, 4, 24, 12))
        cards.grid(row=1, column=0, sticky="ew")
        for column in range(3):
            cards.columnconfigure(column, weight=1)

        card_data = (
            ("Item Types", self.total_items_var, "primary"),
            ("Total Units", self.total_units_var, "success"),
            ("Borrowed", self.borrowed_units_var, "warning"),
        )
        for column, (title, variable, style) in enumerate(card_data):
            card = ttk.Labelframe(cards, text=title, padding=14)
            card.grid(
                row=0,
                column=column,
                sticky="ew",
                padx=(0 if column == 0 else 6, 0 if column == 2 else 6),
            )
            ttk.Label(
                card,
                textvariable=variable,
                font=self.metric_font,
                bootstyle=style,
            ).pack(anchor="w")

        content = ttk.Frame(self.root, padding=(24, 0, 24, 12))
        content.grid(row=2, column=0, sticky="nsew")
        content.columnconfigure(1, weight=1)
        content.rowconfigure(0, weight=1)

        form = ttk.Labelframe(content, text="Add inventory", padding=16)
        form.grid(row=0, column=0, sticky="ns", padx=(0, 8))
        form.columnconfigure(0, weight=1)

        ttk.Label(form, text="Item name").grid(row=0, column=0, sticky="w")
        ttk.Entry(
            form,
            textvariable=self.name_var,
            width=28,
            bootstyle="primary",
        ).grid(row=1, column=0, sticky="ew", pady=(4, 14))

        ttk.Label(form, text="Total quantity").grid(row=2, column=0, sticky="w")
        ttk.Entry(
            form,
            textvariable=self.quantity_var,
            width=28,
            bootstyle="primary",
        ).grid(row=3, column=0, sticky="ew", pady=(4, 14))

        ttk.Button(
            form,
            text="Add Item",
            command=self.add_item,
            bootstyle="primary",
        ).grid(row=4, column=0, sticky="ew")

        ttk.Label(
            form,
            text="Select a row before borrowing or returning one unit.",
            wraplength=220,
            bootstyle="secondary",
        ).grid(row=5, column=0, sticky="w", pady=(18, 0))

        table_panel = ttk.Labelframe(content, text="Inventory", padding=12)
        table_panel.grid(row=0, column=1, sticky="nsew", padx=(8, 0))
        table_panel.columnconfigure(0, weight=1)
        table_panel.rowconfigure(0, weight=1)

        columns = ("name", "quantity", "borrowed", "available", "status")
        self.tree = ttk.Treeview(
            table_panel,
            columns=columns,
            show="headings",
            selectmode="browse",
            height=15,
            bootstyle="primary",
        )
        self.tree.grid(row=0, column=0, sticky="nsew")

        headings = {
            "name": "Item",
            "quantity": "Total",
            "borrowed": "Borrowed",
            "available": "Available",
            "status": "Status",
        }
        widths = {
            "name": 220,
            "quantity": 80,
            "borrowed": 90,
            "available": 90,
            "status": 110,
        }
        for column in columns:
            self.tree.heading(column, text=headings[column])
            self.tree.column(
                column,
                width=widths[column],
                anchor="w" if column == "name" else "center",
            )

        scrollbar = ttk.Scrollbar(
            table_panel,
            orient="vertical",
            command=self.tree.yview,
        )
        scrollbar.grid(row=0, column=1, sticky="ns")
        self.tree.configure(yscrollcommand=scrollbar.set)

        actions = ttk.Frame(table_panel, padding=(0, 12, 0, 0))
        actions.grid(row=1, column=0, columnspan=2, sticky="ew")
        actions.columnconfigure((0, 1), weight=1)

        ttk.Button(
            actions,
            text="Borrow One",
            bootstyle="warning",
        ).grid(row=0, column=0, sticky="ew", padx=(0, 6))

        ttk.Button(
            actions,
            text="Return One",
            bootstyle="success",
        ).grid(row=0, column=1, sticky="ew", padx=(6, 0))

        ttk.Label(
            self.root,
            textvariable=self.status_var,
            padding=(24, 8, 24, 16),
            bootstyle="info",
        ).grid(row=3, column=0, sticky="ew")

    def add_item(self):
        name = self.name_var.get().strip()
        try:
            quantity = int(self.quantity_var.get())
        except ValueError:
            self.set_status("Enter a whole number for quantity.")
            return

        if not name:
            self.set_status("Enter an item name.")
            return
        if quantity <= 0:
            self.set_status("Quantity must be greater than zero.")
            return

        try:
            with self.connection:
                self.connection.execute(
                    "INSERT INTO inventory (name, quantity, borrowed) VALUES (?, ?, 0)",
                    (name, quantity),
                )
        except sqlite3.IntegrityError:
            self.set_status(f"{name} already exists.")
            return

        self.name_var.set("")
        self.quantity_var.set("1")
        self.refresh_inventory()
        self.set_status(f"Added {name}.")

    def refresh_inventory(self):
        for item in self.tree.get_children():
            self.tree.delete(item)

        rows = self.connection.execute(
            "SELECT id, name, quantity, borrowed "
            "FROM inventory ORDER BY name COLLATE NOCASE"
        ).fetchall()

        total_units = 0
        borrowed_units = 0
        for item_id, name, quantity, borrowed in rows:
            available = quantity - borrowed
            status = "Available" if available > 0 else "Out of stock"
            self.tree.insert(
                "",
                "end",
                iid=str(item_id),
                values=(name, quantity, borrowed, available, status),
            )
            total_units += quantity
            borrowed_units += borrowed

        self.total_items_var.set(str(len(rows)))
        self.total_units_var.set(str(total_units))
        self.borrowed_units_var.set(str(borrowed_units))

    def set_status(self, message):
        self.status_var.set(message)

    def close_database(self):
        self.connection.close()

    def run(self):
        self.root.mainloop()


if __name__ == "__main__":
    InventoryApp().run()
Prove that inventory survives a restart

The dashboard now reads its visible state from SQLite. Before you run the app, which seeded inventory row do you expect the database to return?

  • Launch InventoryDesk from the VS Code terminal by running this command:
.\.venv\Scripts\python.exe app.py

What does this command run?

The command launches app.py with the Python executable from your selected .venv environment. InventoryDesk opens against the database under your home directory.

You should see USB-C Dock with a total quantity of 6. Its borrowed count is 1.

  • Enter Wireless Mouse in the Item name field.
  • Enter 4 in the Total quantity field.
  • Click Add Item.

You should see the new row in the table. The summary cards should show two item types and ten total units.

  • Close the InventoryDesk window.

Before you reopen the app, do you expect Wireless Mouse to return with the same dashboard totals?

  • Reopen InventoryDesk by running the same command:
.\.venv\Scripts\python.exe app.py

What does the second launch test?

The second process starts with no access to the previous session's memory. Any restored row must have come from inventory.db.

You should see Wireless Mouse with quantity 4 after the restart. The item count and total unit count should remain unchanged.

Did the row disappear again?

Confirm add_item() calls self.connection.execute() inside the connection context. A direct Treeview insertion only changes the current window.

Check that refresh_inventory() runs immediately after build_interface() during startup.

help me find why my InventoryDesk row disappears after restarting

That persistence test is complete. Your inventory records and dashboard totals now survive application restarts.

Your dashboard now has durable inventory data. Next up, you will connect the Borrow One and Return One buttons to safe stock-changing rules.

Add Safe Borrow and Return Workflows

Your inventory now survives a restart because SQLite stores each record outside the interface. The next goal is to make every stock change safe.

Persistent data can preserve an impossible count just as reliably as a valid one. This step blocks borrowing at zero availability. It also blocks returns at zero borrowed units.

In this step, get ready to:
  • Connect each selected table row to its SQLite record.
  • Block stock changes that would create impossible counts.
  • Verify the completed inventory workflow in both themes.
Connect actions to the selected row

Each Treeview row already uses the database ID as its item identifier. The button callbacks can use that identifier to load the exact SQLite record.

  • In app.py, find def refresh_inventory(self):.
  • Add this method directly above it:
    def selected_item_id(self):
        selection = self.tree.selection()
        if not selection:
            self.set_status("Select an inventory row first.")
            return None
        return int(selection[0])

What does this selection helper do?

  • The selection() call reads the currently highlighted row.
  • An empty selection updates status_var with a clear instruction.
  • A selected row returns the database ID stored in its item identifier.
  • Save app.py.
  • Confirm the new method has valid syntax by running this command:
.\.venv\Scripts\python.exe app.py

You'll see the dashboard open with your persisted inventory rows. This confirms the selection helper did not disrupt startup.

  • Close the InventoryDesk window.

Does the dashboard fail to open?

Check that selected_item_id() lines up with the other methods inside InventoryApp. Python uses indentation to determine which code belongs to the class.

Compare the method name with the snippet before running the application again.

Help me fix the new selection helper in InventoryDesk.

The selection helper identifies the intended record. The stock callback can now load its latest quantities before making a decision.

  • Add the following method directly below selected_item_id() and above refresh_inventory():
    def change_borrowed(self, change):
        item_id = self.selected_item_id()
        if item_id is None:
            return

        row = self.connection.execute(
            "SELECT name, quantity, borrowed FROM inventory WHERE id = ?",
            (item_id,),
        ).fetchone()
        if row is None:
            self.set_status("That item no longer exists.")
            return

How does the callback find the record?

  • The callback stops immediately when no row is selected.
  • The query uses the selected database ID as a placeholder value.
  • A missing record produces inline feedback instead of a crash.
  • In build_interface(), find the existing Borrow One button block.
  • Replace the two stock action button blocks with this code:
        ttk.Button(
            actions,
            text="Borrow One",
            command=lambda: self.change_borrowed(1),
            bootstyle="warning",
        ).grid(row=0, column=0, sticky="ew", padx=(0, 6))

        ttk.Button(
            actions,
            text="Return One",
            command=lambda: self.change_borrowed(-1),
            bootstyle="success",
        ).grid(row=0, column=1, sticky="ew", padx=(6, 0))

What do the button values mean?

The Borrow button sends 1 to represent one additional borrowed unit. The Return button sends -1 to represent one returned unit.

Both buttons now share one callback. This keeps selection handling consistent.

  • Save app.py.
  • Start InventoryDesk by running this command:
.\.venv\Scripts\python.exe app.py
  • Click Borrow One without selecting a row.

You'll see Select an inventory row first. in the status area. The button now reaches the selected-row guard.

  • Close the InventoryDesk window.

Does the button ignore the click?

Check that each command line sits inside the matching button block. Confirm that both callbacks spell change_borrowed exactly.

Help me trace the InventoryDesk button callback.

Enforce the stock rules

Database constraints protect the stored record from impossible values. Inline checks explain why a requested stock change is rejected.

  • In change_borrowed(), find the return below the missing-record status message.
  • Add this code directly below that line:
        name, quantity, borrowed = row
        if change == 1 and borrowed >= quantity:
            self.set_status(f"No available units of {name} remain.")
            return
        if change == -1 and borrowed <= 0:
            self.set_status(f"No borrowed units of {name} can be returned.")
            return

        with self.connection:
            self.connection.execute(
                "UPDATE inventory SET borrowed = ? WHERE id = ?",
                (borrowed + change, item_id),
            )

        action = "Borrowed" if change == 1 else "Returned"
        self.refresh_inventory()
        self.set_status(f"{action} one unit of {name}.")

How are stock changes kept safe?

  • The first check blocks borrowing when the borrowed count has reached the total quantity.
  • The second check blocks returning when the borrowed count is already zero.
  • The update binds the new borrowed count to the selected item ID through query placeholders.
  • A valid update refreshes the table before the status message confirms the action.
  • Save app.py.
  • Start InventoryDesk by running this command:
.\.venv\Scripts\python.exe app.py
  • Enter Boundary Test in the Item name field.
  • Enter 1 in the Total quantity field.
  • Click Add Item.
  • Select the Boundary Test row.
  • Click Borrow One.

You'll see its borrowed count change to 1. Its available count changes to 0.

The row status now reads Out of stock. The Borrowed summary increases immediately.

  • Select the Boundary Test row again.
  • Click Borrow One again.

You'll see a status message explaining that no available units remain. The counts stay unchanged.

  • Click Return One.

You'll see the borrowed count return to 0. The available count returns to 1.

  • Select the Boundary Test row again.
  • Click Return One again.

You'll see a status message explaining that no borrowed units can be returned. The borrowed count remains at zero.

  • Close the InventoryDesk window.

Do the counts stay unchanged?

Confirm that the update block appears after both boundary checks. Check that self.refresh_inventory() appears after the database update.

Help me debug the InventoryDesk stock update.

Refresh every visible result

A safe update should remain clear in every part of the dashboard. ttkbootstrap can also switch the whole interface between its configured light and dark themes.

  • In build_interface(), find the button with text="Toggle Theme".
  • Replace that button block with this code:
        ttk.Button(
            header,
            text="Toggle Theme",
            command=self.root.toggle_theme,
            bootstyle="primary outline",
        ).grid(row=0, column=1, rowspan=2, sticky="e")

How does the theme switch work?

The button calls the application window's toggle_theme() method. The configured theme modes determine which light and dark styles it uses.

The themed controls redraw together. The inventory data stays unchanged.

  • Save app.py.

✔️ Awesome, I've got everything!

Your stock callbacks and theme control are ready. Make sure app.py is saved.

ⓧ I'd like to double check the full code

import sqlite3
from pathlib import Path

import ttkbootstrap as ttk


APP_NAME = "InventoryDesk"
DATA_DIR = Path.home() / ".inventorydesk"
DB_PATH = DATA_DIR / "inventory.db"


class InventoryApp:
    def __init__(self):
        DATA_DIR.mkdir(parents=True, exist_ok=True)
        self.connection = sqlite3.connect(DB_PATH)
        self.create_database()
        self.seed_database()

        self.root = ttk.App(
            title=APP_NAME,
            theme="bootstrap-light",
            size=(1080, 680),
            minsize=(900, 600),
        )
        self.root.set_theme_modes(
            light="bootstrap-light",
            dark="bootstrap-dark",
        )
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(2, weight=1)

        self.title_font = ttk.Font(root=self.root, size=22, weight="bold")
        self.metric_font = ttk.Font(root=self.root, size=19, weight="bold")

        self.name_var = ttk.StringVar(master=self.root)
        self.quantity_var = ttk.StringVar(master=self.root, value="1")
        self.total_items_var = ttk.StringVar(master=self.root, value="0")
        self.total_units_var = ttk.StringVar(master=self.root, value="0")
        self.borrowed_units_var = ttk.StringVar(master=self.root, value="0")
        self.status_var = ttk.StringVar(master=self.root, value="Ready")

        self.build_interface()
        self.refresh_inventory()
        self.root.on_close(self.close_database)

    def create_database(self):
        self.connection.execute(
            """
            CREATE TABLE IF NOT EXISTS inventory (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL UNIQUE COLLATE NOCASE,
                quantity INTEGER NOT NULL CHECK(quantity >= 0),
                borrowed INTEGER NOT NULL DEFAULT 0 CHECK(borrowed >= 0),
                CHECK(borrowed <= quantity)
            )
            """
        )
        self.connection.commit()

    def seed_database(self):
        item_count = self.connection.execute(
            "SELECT COUNT(*) FROM inventory"
        ).fetchone()[0]
        if item_count == 0:
            with self.connection:
                self.connection.execute(
                    "INSERT INTO inventory (name, quantity, borrowed) VALUES (?, ?, ?)",
                    ("USB-C Dock", 6, 1),
                )

    def build_interface(self):
        header = ttk.Frame(self.root, padding=(24, 20, 24, 10))
        header.grid(row=0, column=0, sticky="ew")
        header.columnconfigure(0, weight=1)

        ttk.Label(
            header,
            text=APP_NAME,
            font=self.title_font,
            bootstyle="primary",
        ).grid(row=0, column=0, sticky="w")

        ttk.Label(
            header,
            text="A focused inventory and lending dashboard",
            bootstyle="secondary",
        ).grid(row=1, column=0, sticky="w", pady=(4, 0))

        ttk.Button(
            header,
            text="Toggle Theme",
            command=self.root.toggle_theme,
            bootstyle="primary outline",
        ).grid(row=0, column=1, rowspan=2, sticky="e")

        cards = ttk.Frame(self.root, padding=(24, 4, 24, 12))
        cards.grid(row=1, column=0, sticky="ew")
        for column in range(3):
            cards.columnconfigure(column, weight=1)

        card_data = (
            ("Item Types", self.total_items_var, "primary"),
            ("Total Units", self.total_units_var, "success"),
            ("Borrowed", self.borrowed_units_var, "warning"),
        )
        for column, (title, variable, style) in enumerate(card_data):
            card = ttk.Labelframe(cards, text=title, padding=14)
            card.grid(
                row=0,
                column=column,
                sticky="ew",
                padx=(0 if column == 0 else 6, 0 if column == 2 else 6),
            )
            ttk.Label(
                card,
                textvariable=variable,
                font=self.metric_font,
                bootstyle=style,
            ).pack(anchor="w")

        content = ttk.Frame(self.root, padding=(24, 0, 24, 12))
        content.grid(row=2, column=0, sticky="nsew")
        content.columnconfigure(1, weight=1)
        content.rowconfigure(0, weight=1)

        form = ttk.Labelframe(content, text="Add inventory", padding=16)
        form.grid(row=0, column=0, sticky="ns", padx=(0, 8))
        form.columnconfigure(0, weight=1)

        ttk.Label(form, text="Item name").grid(row=0, column=0, sticky="w")
        ttk.Entry(
            form,
            textvariable=self.name_var,
            width=28,
            bootstyle="primary",
        ).grid(row=1, column=0, sticky="ew", pady=(4, 14))

        ttk.Label(form, text="Total quantity").grid(row=2, column=0, sticky="w")
        ttk.Entry(
            form,
            textvariable=self.quantity_var,
            width=28,
            bootstyle="primary",
        ).grid(row=3, column=0, sticky="ew", pady=(4, 14))

        ttk.Button(
            form,
            text="Add Item",
            command=self.add_item,
            bootstyle="primary",
        ).grid(row=4, column=0, sticky="ew")

        ttk.Label(
            form,
            text="Select a row before borrowing or returning one unit.",
            wraplength=220,
            bootstyle="secondary",
        ).grid(row=5, column=0, sticky="w", pady=(18, 0))

        table_panel = ttk.Labelframe(content, text="Inventory", padding=12)
        table_panel.grid(row=0, column=1, sticky="nsew", padx=(8, 0))
        table_panel.columnconfigure(0, weight=1)
        table_panel.rowconfigure(0, weight=1)

        columns = ("name", "quantity", "borrowed", "available", "status")
        self.tree = ttk.Treeview(
            table_panel,
            columns=columns,
            show="headings",
            selectmode="browse",
            height=15,
            bootstyle="primary",
        )
        self.tree.grid(row=0, column=0, sticky="nsew")

        headings = {
            "name": "Item",
            "quantity": "Total",
            "borrowed": "Borrowed",
            "available": "Available",
            "status": "Status",
        }
        widths = {
            "name": 220,
            "quantity": 80,
            "borrowed": 90,
            "available": 90,
            "status": 110,
        }
        for column in columns:
            self.tree.heading(column, text=headings[column])
            self.tree.column(
                column,
                width=widths[column],
                anchor="w" if column == "name" else "center",
            )

        scrollbar = ttk.Scrollbar(
            table_panel,
            orient="vertical",
            command=self.tree.yview,
        )
        scrollbar.grid(row=0, column=1, sticky="ns")
        self.tree.configure(yscrollcommand=scrollbar.set)

        actions = ttk.Frame(table_panel, padding=(0, 12, 0, 0))
        actions.grid(row=1, column=0, columnspan=2, sticky="ew")
        actions.columnconfigure((0, 1), weight=1)

        ttk.Button(
            actions,
            text="Borrow One",
            command=lambda: self.change_borrowed(1),
            bootstyle="warning",
        ).grid(row=0, column=0, sticky="ew", padx=(0, 6))

        ttk.Button(
            actions,
            text="Return One",
            command=lambda: self.change_borrowed(-1),
            bootstyle="success",
        ).grid(row=0, column=1, sticky="ew", padx=(6, 0))

        ttk.Label(
            self.root,
            textvariable=self.status_var,
            padding=(24, 8, 24, 16),
            bootstyle="info",
        ).grid(row=3, column=0, sticky="ew")

    def add_item(self):
        name = self.name_var.get().strip()
        try:
            quantity = int(self.quantity_var.get())
        except ValueError:
            self.set_status("Enter a whole number for quantity.")
            return

        if not name:
            self.set_status("Enter an item name.")
            return
        if quantity <= 0:
            self.set_status("Quantity must be greater than zero.")
            return

        try:
            with self.connection:
                self.connection.execute(
                    "INSERT INTO inventory (name, quantity, borrowed) VALUES (?, ?, 0)",
                    (name, quantity),
                )
        except sqlite3.IntegrityError:
            self.set_status(f"{name} already exists.")
            return

        self.name_var.set("")
        self.quantity_var.set("1")
        self.refresh_inventory()
        self.set_status(f"Added {name}.")

    def selected_item_id(self):
        selection = self.tree.selection()
        if not selection:
            self.set_status("Select an inventory row first.")
            return None
        return int(selection[0])

    def change_borrowed(self, change):
        item_id = self.selected_item_id()
        if item_id is None:
            return

        row = self.connection.execute(
            "SELECT name, quantity, borrowed FROM inventory WHERE id = ?",
            (item_id,),
        ).fetchone()
        if row is None:
            self.set_status("That item no longer exists.")
            return

        name, quantity, borrowed = row
        if change == 1 and borrowed >= quantity:
            self.set_status(f"No available units of {name} remain.")
            return
        if change == -1 and borrowed <= 0:
            self.set_status(f"No borrowed units of {name} can be returned.")
            return

        with self.connection:
            self.connection.execute(
                "UPDATE inventory SET borrowed = ? WHERE id = ?",
                (borrowed + change, item_id),
            )

        action = "Borrowed" if change == 1 else "Returned"
        self.refresh_inventory()
        self.set_status(f"{action} one unit of {name}.")

    def refresh_inventory(self):
        for item in self.tree.get_children():
            self.tree.delete(item)

        rows = self.connection.execute(
            "SELECT id, name, quantity, borrowed "
            "FROM inventory ORDER BY name COLLATE NOCASE"
        ).fetchall()

        total_units = 0
        borrowed_units = 0
        for item_id, name, quantity, borrowed in rows:
            available = quantity - borrowed
            status = "Available" if available > 0 else "Out of stock"
            self.tree.insert(
                "",
                "end",
                iid=str(item_id),
                values=(name, quantity, borrowed, available, status),
            )
            total_units += quantity
            borrowed_units += borrowed

        self.total_items_var.set(str(len(rows)))
        self.total_units_var.set(str(total_units))
        self.borrowed_units_var.set(str(borrowed_units))

    def set_status(self, message):
        self.status_var.set(message)

    def close_database(self):
        self.connection.close()

    def run(self):
        self.root.mainloop()


if __name__ == "__main__":
    InventoryApp().run()

Before you run the finished app, which visible values do you expect to change after one unit is borrowed?

  • Start the completed InventoryDesk application by running this command:
.\.venv\Scripts\python.exe app.py
  • Select the Boundary Test row.
  • Click Borrow One.

You'll see the borrowed count become 1. The available count becomes 0.

You'll also see Out of stock in the row. The Borrowed summary increases at the same time.

  • Select the Boundary Test row again.
  • Click Borrow One again.

You'll see the no-availability message. The table values remain within their valid limits.

  • Click Return One.

You'll see the item return to Available. The table values and Borrowed summary update immediately.

  • Select the Boundary Test row again.
  • Click Return One again.
  • Click Toggle Theme.

You'll see the zero-borrowed message before the interface switches theme. The controls and inventory data remain readable in the new theme.

Does one part of the final check fail?

Check the matching button's command value when a click has no effect. Check the call to refresh_inventory() when the database changes without a visible update.

Confirm that the theme button calls self.root.toggle_theme without parentheses.

Help me diagnose the failed InventoryDesk verification.

That completes the inventory workflow. Your app now protects stock limits while keeping every visible count in sync.

Your inventory app is complete. Next up, you'll bundle it into a Windows application and wrap it in an installer.

Build the Windows App and Installer

Your complete InventoryDesk app now manages inventory changes safely. Its local database preserves every valid update between launches.

Another Windows user still needs a Python development environment to run app.py. Packaging removes that dependency from the launch experience.

In this step, PyInstaller creates a standalone application bundle. Inno Setup turns that bundle into installable Windows software.

In this step, get ready to:
  • Build a one-folder Windows application bundle.
  • Define a per-user installer with shortcuts.
  • Install InventoryDesk through its compiled setup program.
Build a one-folder Windows application

Your application depends on ttkbootstrap for its interface. A requirements file records the exact package versions used to create the tested bundle.

  • Right-click the inventorydesk folder in the Visual Studio Code Explorer sidebar.
  • Select New File.
  • Name the file requirements.txt.

You should see requirements.txt open beside app.py.

  • Pin the application dependencies by copying this content into requirements.txt:
ttkbootstrap==2.2.2
pyinstaller==6.22.2

What does this file do?

  • The first line pins ttkbootstrap to version 2.2.2.
  • The second line pins pyinstaller to version 6.22.2.
  • These pins make the dependency list match the environment used to test InventoryDesk.
  • Save requirements.txt with Ctrl+S.
  • Confirm the Explorer sidebar lists requirements.txt beside app.py.

Missing the requirements file?

Check that requirements.txt sits directly inside the inventorydesk folder. Remove any accidental .txt duplication from its name.

Ask for help if the file is saved in the wrong location. Help me place requirements.txt beside app.py in my InventoryDesk project.

✔️ Awesome, I've got everything!

Great. Your dependency versions are recorded in requirements.txt.

ⓧ I'd like to double check the full code

ttkbootstrap==2.2.2
pyinstaller==6.22.2

A one-folder build keeps the executable with its Python runtime dependencies. The installer packages that whole directory for the recipient.

  • Build the InventoryDesk bundle from the existing Visual Studio Code terminal by running:
.\.venv\Scripts\pyinstaller.exe --noconfirm --clean --onedir --windowed --name InventoryDesk --collect-all ttkbootstrap app.py

What does this command do?

  • The --clean option clears cached build work before packaging.
  • The --onedir option creates a distributable application folder.
  • The --windowed option launches InventoryDesk without a terminal window.
  • The --name InventoryDesk option names the bundle.
  • The --collect-all ttkbootstrap option includes the package resources needed by the themed interface.

The build can take a little while because PyInstaller collects the interpreter and application dependencies. The terminal returns to its prompt when the bundle is ready.

  • Expand dist in the Visual Studio Code Explorer sidebar.
  • Expand InventoryDesk inside dist.
  • Right-click InventoryDesk.exe.
  • Choose the option that reveals the executable in File Explorer.
  • Double-click InventoryDesk.exe.

InventoryDesk opens as the same themed dashboard you tested in development. This launch comes from dist\InventoryDesk.

  • Enter Bundle Test in the Item name field.
  • Enter 2 in the Total quantity field.
  • Click Add Item.

You should see Bundle Test in the inventory table. The summary cards update immediately.

  • Close InventoryDesk.
  • Double-click InventoryDesk.exe again in File Explorer.

You should see Bundle Test return after the bundled app reopens. This confirms the executable can use the existing database.

Bundle not opening?

Confirm that you launched InventoryDesk.exe from inside dist\InventoryDesk. Keep the supporting files in that folder beside the executable.

Recheck the terminal output for a packaging failure if the dist\InventoryDesk folder is missing. Help me diagnose why my PyInstaller InventoryDesk bundle did not open.

Define the installation experience

An installer gives InventoryDesk a standard installation directory. It also adds shortcuts and uninstall support.

  • Right-click the inventorydesk folder in the Visual Studio Code Explorer sidebar.
  • Select New File.
  • Name the file installer.iss.

You should see the empty installer.iss file open in the editor.

  • Define the installation experience by copying this script into installer.iss:
[Setup]
AppName=InventoryDesk
AppVersion=1.0.0
DefaultDirName={localappdata}\Programs\InventoryDesk
PrivilegesRequired=lowest
OutputDir=installer_output
OutputBaseFilename=InventoryDesk-Setup
Compression=lzma2
SolidCompression=yes
WizardStyle=modern dynamic
Uninstallable=yes

[Tasks]
Name: "desktopicon"; Description: "Create a desktop shortcut"; Flags: unchecked

[Files]
Source: "dist\InventoryDesk\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs

[Icons]
Name: "{autoprograms}\InventoryDesk"; Filename: "{app}\InventoryDesk.exe"
Name: "{autodesktop}\InventoryDesk"; Filename: "{app}\InventoryDesk.exe"; Tasks: desktopicon

[Run]
Filename: "{app}\InventoryDesk.exe"; Description: "Launch InventoryDesk"; Flags: postinstall nowait skipifsilent

How does the installer work?

  • The DefaultDirName entry places InventoryDesk in the current user's local application data.
  • The PrivilegesRequired=lowest entry prevents an administrator elevation request.
  • The WizardStyle=modern dynamic entry adapts the setup wizard to the Windows light or dark setting.
  • The [Files] section includes the complete dist\InventoryDesk directory.
  • The [Icons] section creates the Start menu shortcut.
  • The Uninstallable=yes entry includes uninstall support.
  • Save installer.iss with Ctrl+S.
  • Confirm the Explorer sidebar lists installer.iss beside requirements.txt.

Installer script saved incorrectly?

Check that the filename ends with .iss. Confirm the script sits directly inside the inventorydesk folder.

Compare every section with the full reference below if the compiler cannot read the script. Help me find a formatting problem in my InventoryDesk Inno Setup script.

✔️ Awesome, I've got everything!

Your installer script now defines the application files and installation behavior.

ⓧ I'd like to double check the full code

[Setup]
AppName=InventoryDesk
AppVersion=1.0.0
DefaultDirName={localappdata}\Programs\InventoryDesk
PrivilegesRequired=lowest
OutputDir=installer_output
OutputBaseFilename=InventoryDesk-Setup
Compression=lzma2
SolidCompression=yes
WizardStyle=modern dynamic
Uninstallable=yes

[Tasks]
Name: "desktopicon"; Description: "Create a desktop shortcut"; Flags: unchecked

[Files]
Source: "dist\InventoryDesk\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs

[Icons]
Name: "{autoprograms}\InventoryDesk"; Filename: "{app}\InventoryDesk.exe"
Name: "{autodesktop}\InventoryDesk"; Filename: "{app}\InventoryDesk.exe"; Tasks: desktopicon

[Run]
Filename: "{app}\InventoryDesk.exe"; Description: "Launch InventoryDesk"; Flags: postinstall nowait skipifsilent
Compile and test the installer

Compiling transforms installer.iss into the setup program another Windows user can run. The resulting installer contains the complete one-folder bundle.

  • Press the Windows key to reach Windows search.
  • Type Inno Setup Compiler.
  • Press Enter to return to Inno Setup Compiler.
  • Open installer.iss in Inno Setup Compiler.
  • Select Compile.

When compilation finishes, you should see InventoryDesk-Setup.exe inside installer_output.

Installer did not compile?

Confirm that dist\InventoryDesk\InventoryDesk.exe exists before compiling. The installer script needs the complete bundle as its source.

Check the highlighted script line for a missing quote or section heading. Help me diagnose why my InventoryDesk Inno Setup script did not compile.

This setup installs InventoryDesk for your Windows account without requesting administrator elevation. Its uninstaller can remove the installed program later.

  • Return to File Explorer from earlier.
  • Open installer_output inside the inventorydesk folder.
  • Double-click InventoryDesk-Setup.exe.
  • Choose whether to enable Create a desktop shortcut.
  • Keep Launch InventoryDesk selected.
  • Complete the installation.

InventoryDesk opens from the installed application files. You do not need to run app.py.

  • Close InventoryDesk.
  • Minimize Visual Studio Code.

Before you launch it from Start, do you expect InventoryDesk to need Visual Studio Code or a terminal?

  • Press the Windows key.
  • Type InventoryDesk.
  • Press Enter.

You should see the complete InventoryDesk dashboard without opening Visual Studio Code or a terminal. The Start menu shortcut now launches the installed application.

  • Close InventoryDesk.
  • Return to Windows search.
  • Open Windows' installed-apps list.
  • Enter InventoryDesk in the list's search field.

You should see InventoryDesk in the installed-apps list with an uninstall action. This confirms Windows recognizes it as installed software.

The installed application files live in the per-user installation directory. Your SQLite data remains at %USERPROFILE%\.inventorydesk\inventory.db outside that directory.

You have turned a development script into installed Windows software. InventoryDesk now launches through its own shortcut and supports standard uninstallation.

Secret mission

Prove the Installation Lifecycle

Test InventoryDesk as a user through a full installation lifecycle. Prove that inventory survives a reinstall. Then remove the data deliberately and prove that the app starts clean.

Clean Up Your Resources

Clean Up Your Resources

InventoryDesk keeps its resources on your Windows computer, so there are no ongoing costs for personal learning use. Decide whether to keep the project, pause by closing the app, or delete the installed program and its data.

Resources you used:

  • The inventorydesk project folder containing app.py, requirements.txt, installer.iss, and .venv.
  • The PyInstaller work in build and the application bundle in dist\InventoryDesk.
  • The compiled Inno Setup installer at installer_output\InventoryDesk-Setup.exe.
  • The installed InventoryDesk program with its Start menu shortcut and any optional desktop shortcut.
  • The persistent SQLite database at %USERPROFILE%\.inventorydesk\inventory.db.

Keep everything running

No action is needed. Choose this if you want InventoryDesk available for continued use or future development.

  • Keep InventoryDesk installed so its Start menu shortcut remains available.
  • Keep %USERPROFILE%\.inventorydesk\inventory.db so your inventory remains available between launches.
  • Keep the inventorydesk project folder so you can rebuild the application bundle or installer.

Pause - I'll come back to this later

Close InventoryDesk to stop its running process. Your project files and database stay on disk for when you return.

  • Close InventoryDesk if its window is open.
  • Leave %USERPROFILE%\.inventorydesk\inventory.db in place to preserve the current inventory.
  • Return later by launching InventoryDesk from its Start menu shortcut.

Delete - I don't want to use this again

Remove all project resources to start fresh. Deleting %USERPROFILE%\.inventorydesk permanently erases the inventory database.

  • Close InventoryDesk if it is running.
  • Open Windows' installed-apps list.
  • Select InventoryDesk in the installed-apps list.
  • Choose Uninstall to remove the installed program files and shortcuts.
  • Use File Explorer to delete the inventorydesk project folder from the location where you created it.
  • Use File Explorer to delete %USERPROFILE%\.inventorydesk.
  • Confirm that InventoryDesk no longer appears in Windows' installed-apps list.
  • Confirm that %USERPROFILE%\.inventorydesk no longer exists.

Nice Work!

Nice Work!

You made it! InventoryDesk now runs as a polished Windows inventory app with durable stock data. Its installer lets another Windows user launch it without a development environment.

You've learned how to:

  • Create a modern dashboard with ttkbootstrap widgets. Give users live summaries plus validated controls for changing inventory.
  • Build persistent inventory with SQLite. Keep user data separate from installed program files.
  • Bundle InventoryDesk with PyInstaller. Deliver it through an Inno Setup installer that creates shortcuts. Include uninstall support.
  • Secret Mission: Prove the installation lifecycle by confirming that user data survives reinstalling. Verify that deliberate database cleanup restores only the seeded item.

Ready to quiz yourself?