Build an Inventory Task Manager

Build a password-protected desktop app for inventory and linked tasks.

Introduction

30 Second Summary

A stock list only helps when it stays current. Teams also need a clear way to connect each item with the work it requires.

In this project, you will build Company Inventory Manager as a Windows desktop application written in Python. You will use Tkinter for the interface plus SQLite for persistent inventory records behind account login.

What You'll Build

Launch Company Inventory Manager to sign in before viewing inventory that survives restarts with related tasks that move from open to done.

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

  • A protected sign-in flow where you create an account through masked password fields before opening the dashboard.
  • A persistent inventory dashboard where added items remain visible after you close the application and reopen it.
  • A linked task workflow where you assign work to an inventory item before marking it done. A Record Details view displays descriptions from both record types through polymorphism.
  • Secret Mission: highlight inventory rows when their quantity is five or lower.

Are there any prerequisites?

You only need a Windows computer. The guide covers every required installation before you write any code.

Before We Start

Before the hands-on work begins, lock in what the Company Inventory Manager will track and why its login protection matters. This gives every later part of the application a clear purpose.

Set Up Your Windows Development Environment

The Company Inventory Manager depends on a verified Python interpreter. Its interface also depends on Tkinter. Its saved data later depends on SQLite.

Environment problems can look like application bugs. This step confirms that every required tool works before you create any source files.

In this step, get ready to:
  • Install Python 3.15.0 through the Windows Python Install Manager.
  • Verify Tkinter with Tcl/Tk 9.0 plus the Python sqlite3 module.
  • Install Visual Studio Code 1.141.0 before opening the company-inventory-manager folder.
Install and verify Python

The Windows Python Install Manager controls which Python runtimes are available. The runtime list lets you confirm the required version before testing its standard-library tools.

  • Press the Windows key to open Windows search.
  • Type PowerShell into the search field.
  • Press Enter to open PowerShell.
  • Check the installed Python runtimes by running this command:
py list

What does this command show?

The Windows Python Install Manager lists every installed Python runtime. You need an entry for Python 3.15.0 before continuing.

✔️ I see the required Python version

Good. Python 3.15.0 is installed through the Windows Python Install Manager.

ⓧ I see an older Python version

Your computer has Python installed. The project requires Python 3.15.0 so every standard-library feature matches the project code.

  • Open the official Python downloads for Windows page in your browser.
  • Download the current Python Install Manager.
  • Run the downloaded installer.
  • Allow the Windows permission request if it appears.
  • Install Python 3.15.0.
  • Select the regular Windows runtime instead of the embeddable distribution.
  • Close PowerShell after the installation finishes.
  • Reopen PowerShell through Windows search.

ⓧ The Python command is unavailable

The Windows Python Install Manager is missing or unavailable from your terminal. Installing it provides the global Python commands used throughout this project.

  • Open the official Python downloads for Windows page in your browser.
  • Download the current Python Install Manager.
  • Run the downloaded installer.
  • Allow the Windows permission request if it appears.
  • Install Python 3.15.0.
  • Select the regular Windows runtime instead of the embeddable distribution.
  • Close PowerShell after the installation finishes.
  • Reopen PowerShell through Windows search.
  • Run the runtime check again after completing the matching tab:
py list

What confirms Python is ready?

The runtime list should now contain Python 3.15.0. This confirms that the required interpreter is available through the Windows Python Install Manager.

Still missing the Python runtime?

  • Confirm that you selected the regular Windows runtime during installation.
  • Restart PowerShell so it can read the updated command path.

Ask for help: Help me check why the Windows Python Install Manager does not list Python 3.15.0.

Tkinter provides the desktop window toolkit used by the application. Official Python binary releases include Tcl/Tk 9.0 for this interface.

  • Open the Tkinter demonstration window by running this command:
python -m tkinter

What does this test prove?

The command imports Tkinter through your active Python runtime. A demonstration window confirms that Python can reach the bundled Tcl/Tk toolkit.

You should see a small Tk demonstration window that displays the installed Tcl/Tk version.

  • Confirm that the demonstration window reports Tcl/Tk 9.0.
  • Close the demonstration window.
  • Check the SQLite library version by running this command:
python -m sqlite3 -v

What does this version check prove?

The Python sqlite3 module connects Python code to the bundled SQLite library. A printed version confirms that the database interface is available without installing another package.

You should see an SQLite version number in PowerShell. That output confirms the local database layer is ready.

Did a Python check fail?

  • Confirm that the runtime list contains Python 3.15.0.
  • Confirm that you installed the regular Windows runtime instead of the embeddable distribution.

Ask for help: Help me diagnose my Tkinter or sqlite3 verification failure on Python 3.15.0.

Install and verify Visual Studio Code

Visual Studio Code gives you one workspace for the project files plus its terminal. Its command-line launcher also opens the current folder directly from PowerShell.

  • Check the installed Visual Studio Code version by running this command in PowerShell:
code --version

What does this command show?

The Visual Studio Code command-line interface prints the installed version first. It also prints build information for the editor.

✔️ I see the required editor version

That check confirms Visual Studio Code 1.141.0 is installed. Its command-line launcher is also available from PowerShell.

ⓧ I see an older editor version

Your command-line launcher works. Update the editor to Visual Studio Code 1.141.0 before opening the project.

  • Open the official Visual Studio Code download page in your browser.
  • Download Visual Studio Code 1.141.0 with the recommended Windows User setup.
  • Run the downloaded installer.
  • Complete the Windows User setup.
  • Close PowerShell after the installation finishes.
  • Reopen PowerShell through Windows search.

ⓧ The code command is unavailable

Visual Studio Code is missing or unavailable through the command path. The recommended Windows User setup adds the editor command to that path.

  • Open the official Visual Studio Code download page in your browser.
  • Download Visual Studio Code 1.141.0 with the recommended Windows User setup.
  • Run the downloaded installer.
  • Complete the Windows User setup.
  • Close PowerShell after the installation finishes.
  • Reopen PowerShell through Windows search.
  • Run the editor version check again after completing the matching tab:
code --version

What confirms the editor is ready?

The first output line should show 1.141.0. That result confirms both the installed editor version and its command-line launcher.

Still unable to run the editor command?

  • Confirm that you used the recommended Windows User setup.
  • Restart PowerShell so it can load the updated command path.

Ask for help: Help me make the Visual Studio Code command available in PowerShell.

Create and open the project folder

A dedicated folder keeps the application code beside its future database file. PowerShell can create that folder on your Desktop before Visual Studio Code opens it as a workspace.

  • Move to your Desktop by running the commands below.
  • Create the company-inventory-manager folder through the same command block.
  • Enter the new folder through the same command block:
Set-Location -Path "$env:USERPROFILE\Desktop"
New-Item -Path ".\company-inventory-manager" -ItemType Directory
Set-Location -Path ".\company-inventory-manager"

What do these commands do?

  • The first command moves PowerShell to your Windows Desktop.
  • The second command creates the company-inventory-manager directory.
  • The third command makes that directory the active PowerShell location.

PowerShell should display the new directory during creation. Your prompt should now end with company-inventory-manager.

  • Open the active project folder in Visual Studio Code by running this command:
code .

What does this command open?

The editor command launches Visual Studio Code. The period tells it to use the current company-inventory-manager folder as the workspace.

You should see company-inventory-manager in the Visual Studio Code Explorer. The folder should contain no source files or database yet.

  • Select Terminal from the Visual Studio Code menu bar.
  • Select New Terminal from the menu.

The integrated terminal should open at the company-inventory-manager folder. This is where you run the project throughout the remaining steps.

Before you run the final check, which outputs should confirm that the interpreter, GUI toolkit, database module, and editor are all ready?

  • Run all four environment checks from the Visual Studio Code integrated terminal:
  • Close the Tk demonstration window when it opens so the remaining checks can continue.
py list
python -m tkinter
python -m sqlite3 -v
code .

What does the final check confirm?

  • The runtime list confirms Python 3.15.0 is installed.
  • The demonstration window confirms Tkinter can use Tcl/Tk 9.0.
  • The printed database version confirms the Python sqlite3 module can reach SQLite.
  • The editor command confirms Visual Studio Code can open the current folder.

Your environment is working when you see the Python runtime plus an SQLite version. You should also see the Tk demonstration window before Visual Studio Code opens the project folder.

Did the final check stop early?

  • Close the Tk demonstration window so PowerShell can continue to the next command.
  • Confirm that the integrated terminal prompt ends with company-inventory-manager.
  • Restart Visual Studio Code if a recently installed command remains unavailable.

Ask for help: Help me diagnose which Windows environment check is failing.

Your Windows workspace is ready for development. Next, you'll build the first object-oriented inventory prototype and see it running in a desktop window.

Build the OOP Inventory Prototype

Your verified Windows environment can now run Python desktop code from the company-inventory-manager folder in Visual Studio Code. The next goal is a visible inventory screen built on reusable business objects.

This first version keeps its records in a Python list. You will restart the application to test whether memory alone can preserve company inventory.

In this step, get ready to:
  • Define an abstract record model with private data and read-only properties.
  • Create inventory item and inventory task subclasses with their own detail methods.
  • Run a Tkinter inventory form and test what happens after a restart.
Define the record abstraction

An abstraction captures the features that every company record shares. Encapsulation keeps each record's internal values behind properties.

  • Create models.py inside the open company-inventory-manager folder by using the new-file control in the Visual Studio Code sidebar.
  • Define the shared record structure by adding this code to models.py:
from abc import ABC, abstractmethod


class CompanyRecord(ABC):
    def __init__(self, record_id: int | None, title: str):
        self.__record_id = record_id
        self.__title = title.strip()

    @property
    def record_id(self) -> int | None:
        return self.__record_id

    @property
    def title(self) -> str:
        return self.__title

    @abstractmethod
    def get_details(self) -> str:
        pass

What does this code do?

  • CompanyRecord inherits from ABC. This makes it the shared abstract base for company records.
  • __record_id and __title hold data that callers cannot replace directly.
  • record_id and title expose those values through read-only properties.
  • get_details() requires every concrete subclass to provide its own description.
  • Save models.py.
  • Inspect models.py to confirm that CompanyRecord contains two properties and one abstract method.

Seeing an editor warning?

Check that ABC and abstractmethod both appear in the import. Confirm that every method inside CompanyRecord uses the same four-space indentation.

Ask for help: Help me find the syntax or indentation problem in my CompanyRecord class.

Add concrete inventory records

Inheritance lets each inventory record reuse the common identifier and title. Method overriding gives each subclass a description suited to its own fields.

  • Add the InventoryItem class below CompanyRecord in models.py by copying this code:
class InventoryItem(CompanyRecord):
    def __init__(
        self,
        record_id: int | None,
        title: str,
        quantity: int,
        location: str,
    ):
        super().__init__(record_id, title)
        self.__quantity = quantity
        self.__location = location.strip()

    @property
    def quantity(self) -> int:
        return self.__quantity

    @property
    def location(self) -> str:
        return self.__location

    def get_details(self) -> str:
        return (
            f"Inventory: {self.title} | Quantity: {self.quantity} | "
            f"Location: {self.location}"
        )

How does an inventory item work?

  • super().__init__() sends the shared identifier and title to CompanyRecord.
  • __quantity and __location keep item-specific data private.
  • get_details() overrides the abstract method with an inventory description.
  • Save models.py.
  • Inspect the InventoryItem class to confirm that it has read-only quantity and location properties.
  • Add the main body of InventoryTask below InventoryItem by copying this code:
class InventoryTask(CompanyRecord):
    def __init__(
        self,
        record_id: int | None,
        title: str,
        item_id: int,
        item_name: str,
        status: str,
    ):
        super().__init__(record_id, title)
        self.__item_id = item_id
        self.__item_name = item_name.strip()
        self.__status = status

    @property
    def item_id(self) -> int:
        return self.__item_id

    @property
    def item_name(self) -> str:
        return self.__item_name

    @property
    def status(self) -> str:
        return self.__status

What does the task store?

  • item_id identifies the inventory item associated with the task.
  • item_name provides a readable item name for the interface.
  • status records whether the task is open or complete.
  • Complete InventoryTask by adding this method directly below the status property:
    def get_details(self) -> str:
        return (
            f"Task: {self.title} | Item: {self.item_name} | "
            f"Status: {self.status}"
        )

Why override the same method?

Both subclasses respond to get_details(). Each response uses fields that belong to that specific record type.

This shared method supports polymorphism. Later code can request record details without checking which subclass it received.

  • Save the completed models.py file.
  • Compare the indentation of both get_details() methods to confirm that each one belongs to its subclass.

Is a method outside its class?

Check that every class method starts with four spaces. Check that each method body starts with eight spaces.

Ask for help: Help me correct the class indentation in models.py.

✔️ Awesome, I've got everything!

Great. Save models.py before building the interface.

ⓧ I'd like to double check the full code

Compare your complete models.py file with this reference.

from abc import ABC, abstractmethod


class CompanyRecord(ABC):
    def __init__(self, record_id: int | None, title: str):
        self.__record_id = record_id
        self.__title = title.strip()

    @property
    def record_id(self) -> int | None:
        return self.__record_id

    @property
    def title(self) -> str:
        return self.__title

    @abstractmethod
    def get_details(self) -> str:
        pass


class InventoryItem(CompanyRecord):
    def __init__(
        self,
        record_id: int | None,
        title: str,
        quantity: int,
        location: str,
    ):
        super().__init__(record_id, title)
        self.__quantity = quantity
        self.__location = location.strip()

    @property
    def quantity(self) -> int:
        return self.__quantity

    @property
    def location(self) -> str:
        return self.__location

    def get_details(self) -> str:
        return (
            f"Inventory: {self.title} | Quantity: {self.quantity} | "
            f"Location: {self.location}"
        )


class InventoryTask(CompanyRecord):
    def __init__(
        self,
        record_id: int | None,
        title: str,
        item_id: int,
        item_name: str,
        status: str,
    ):
        super().__init__(record_id, title)
        self.__item_id = item_id
        self.__item_name = item_name.strip()
        self.__status = status

    @property
    def item_id(self) -> int:
        return self.__item_id

    @property
    def item_name(self) -> str:
        return self.__item_name

    @property
    def status(self) -> str:
        return self.__status

    def get_details(self) -> str:
        return (
            f"Task: {self.title} | Item: {self.item_name} | "
            f"Status: {self.status}"
        )

What should match?

Your file should define one abstract base class and two concrete subclasses. The private fields and property names should match this reference exactly.

Build and restart the inventory form

An event-driven GUI waits for actions such as a button click. This prototype responds by creating an InventoryItem and placing it in an in-memory list.

  • Create myscript.py inside the open company-inventory-manager folder by using the new-file control in the Visual Studio Code sidebar.
  • Add the in-memory item collection and button handler by copying this code into myscript.py:
import tkinter as tk
from tkinter import messagebox, ttk

from models import InventoryItem

# Keep records in memory for this first prototype
items = []


def add_item() -> None:
    name = item_name.get().strip()
    location = item_location.get().strip()
    try:
        quantity = int(item_quantity.get())
    except ValueError:
        quantity = -1

    if not name or not location or quantity < 0:
        messagebox.showerror("Invalid Item", "Enter a name, a non-negative whole quantity, and a location.")
        return

    # Convert the form values into a business object
    item = InventoryItem(None, name, quantity, location)
    items.append(item)
    inventory_list.insert(tk.END, item.get_details())
    item_name.set("")
    item_quantity.set("")
    item_location.set("")

How does the button handler work?

  • items holds every inventory object created during the current run.
  • add_item() reads the form values and rejects incomplete or negative entries.
  • InventoryItem converts valid form values into a business object.
  • inventory_list.insert() displays the result of the overridden get_details() method.
  • Save myscript.py.
  • Inspect the add_item() function to confirm that it appends each valid object to items.

Seeing undefined-name warnings?

The form variables are defined in the next code chunk. Confirm that the names in add_item() match item_name, item_quantity, item_location, and inventory_list exactly.

Ask for help: Help me compare the variable names in my add_item function.

  • Build the inventory window below add_item() by copying this code:
# Build the inventory window and input form
root = tk.Tk()
root.title("Company Inventory Manager")
root.geometry("900x620")
root.minsize(760, 520)

form = ttk.LabelFrame(root, text="Add Inventory Item", padding=10)
form.pack(fill="x", padx=12, pady=12)

item_name = tk.StringVar()
item_quantity = tk.StringVar()
item_location = tk.StringVar()

ttk.Label(form, text="Name").grid(row=0, column=0, sticky="w")
ttk.Label(form, text="Quantity").grid(row=0, column=1, sticky="w")
ttk.Label(form, text="Location").grid(row=0, column=2, sticky="w")
ttk.Entry(form, textvariable=item_name).grid(row=1, column=0, padx=(0, 8))
ttk.Entry(form, textvariable=item_quantity).grid(row=1, column=1, padx=(0, 8))
ttk.Entry(form, textvariable=item_location).grid(row=1, column=2, padx=(0, 8))
ttk.Button(form, text="Add Item", command=add_item).grid(row=1, column=3)

# Show each in-memory record below the form
inventory_list = tk.Listbox(root, font=("Consolas", 10))
inventory_list.pack(fill="both", expand=True, padx=12, pady=(0, 12))

root.mainloop()

What builds the visible interface?

  • tk.Tk() creates the application window.
  • StringVar objects carry text between the three entry fields and the button handler.
  • Add Item calls add_item() when selected.
  • root.mainloop() keeps the window responsive to user actions.
  • Save the completed myscript.py file.
  • Compare the form field names with the variables used inside add_item().

Does the form code show warnings?

Confirm that the window code sits below the complete add_item() function. Check that root.mainloop() is the final line.

Ask for help: Help me find the syntax problem in my Tkinter form code.

✔️ Awesome, I've got everything!

Your model and prototype files are ready. Save both files before running the application.

ⓧ I'd like to double check the full code

Compare your complete myscript.py file with this reference.

import tkinter as tk
from tkinter import messagebox, ttk

from models import InventoryItem

# Keep records in memory for this first prototype
items = []


def add_item() -> None:
    name = item_name.get().strip()
    location = item_location.get().strip()
    try:
        quantity = int(item_quantity.get())
    except ValueError:
        quantity = -1

    if not name or not location or quantity < 0:
        messagebox.showerror("Invalid Item", "Enter a name, a non-negative whole quantity, and a location.")
        return

    # Convert the form values into a business object
    item = InventoryItem(None, name, quantity, location)
    items.append(item)
    inventory_list.insert(tk.END, item.get_details())
    item_name.set("")
    item_quantity.set("")
    item_location.set("")


# Build the inventory window and input form
root = tk.Tk()
root.title("Company Inventory Manager")
root.geometry("900x620")
root.minsize(760, 520)

form = ttk.LabelFrame(root, text="Add Inventory Item", padding=10)
form.pack(fill="x", padx=12, pady=12)

item_name = tk.StringVar()
item_quantity = tk.StringVar()
item_location = tk.StringVar()

ttk.Label(form, text="Name").grid(row=0, column=0, sticky="w")
ttk.Label(form, text="Quantity").grid(row=0, column=1, sticky="w")
ttk.Label(form, text="Location").grid(row=0, column=2, sticky="w")
ttk.Entry(form, textvariable=item_name).grid(row=1, column=0, padx=(0, 8))
ttk.Entry(form, textvariable=item_quantity).grid(row=1, column=1, padx=(0, 8))
ttk.Entry(form, textvariable=item_location).grid(row=1, column=2, padx=(0, 8))
ttk.Button(form, text="Add Item", command=add_item).grid(row=1, column=3)

# Show each in-memory record below the form
inventory_list = tk.Listbox(root, font=("Consolas", 10))
inventory_list.pack(fill="both", expand=True, padx=12, pady=(0, 12))

root.mainloop()

What should match?

Your file should import InventoryItem and keep records in the items list. It should create three input fields with an inventory display below them.

  • Run the prototype from the integrated terminal by using this command:
python myscript.py

What should you see?

You should see a window titled Company Inventory Manager. The window should contain name, quantity, and location fields above an empty inventory list.

Did the window fail to open?

Check the terminal for the first referenced line in models.py or myscript.py. Compare that line with the full-code references above.

Ask for help: Help me debug why my inventory prototype does not open.

  • Enter Laptop in the Name field.
  • Enter 3 in the Quantity field.
  • Enter Office in the Location field.
  • Select Add Item.

You should see Inventory: Laptop | Quantity: 3 | Location: Office in the list. Your form now creates and displays an InventoryItem object.

Before you restart the application, do you expect the Laptop entry to return?

  • Close the Company Inventory Manager window by using its title-bar close control.
  • Restart the prototype from the integrated terminal by running:
python myscript.py

What did the restart reveal?

You should see the same form with an empty inventory list. The items list existed only in the previous Python process.

Closing the application removed that process from memory. No database has stored the Laptop record.

You found the prototype's intended limitation. The interface works while the application is open, but its inventory cannot survive a restart.

Did the old item remain visible?

Confirm that you closed the original window before running the command again. A second item added after the restart should still disappear after another complete close.

Ask for help: Help me confirm whether my prototype is restarting with a fresh in-memory list.

Your object model and visible inventory form now work together. Next, you will replace temporary memory with SQLite storage so inventory survives every restart.

Store Inventory with SQLite

The in-memory prototype proved that your Tkinter form works. It also exposed a serious gap because every inventory item disappears when the application closes.

This step connects the dashboard to SQLite. Your application will store inventory on disk through parameterized operations.

In this step, get ready to:
  • Create the database connection with relational tables.
  • Add parameterized operations for saving inventory items.
  • Connect the dashboard to persistent inventory storage.
Create the database schema

A relational schema defines how your application stores each type of record. The schema also establishes foreign keys between related tables.

  • Select the New File button in the Visual Studio Code Explorer sidebar.
  • Name the file database.py.
  • Create the database connection with the first table by entering this code:
import sqlite3

from models import InventoryItem, InventoryTask


class DatabaseManager:
    def __init__(self, database_name: str = "company_inventory.db"):
        self.__connection = sqlite3.connect(database_name)
        self.__connection.row_factory = sqlite3.Row
        self.__connection.execute("PRAGMA foreign_keys = ON")
        self.__create_tables()

    def __create_tables(self) -> None:
        self.__connection.executescript(
            """
            CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY,
                username TEXT NOT NULL UNIQUE,
                salt BLOB NOT NULL,
                password_hash BLOB NOT NULL
            );
            """
        )

What does this code do?

  • The private __connection field opens company_inventory.db when DatabaseManager is created.
  • The sqlite3.Row row factory lets later methods read query results by column name.
  • The PRAGMA foreign_keys = ON statement enables relationship checks for this connection.
  • The users table prepares the database for the account protection you add later.
  • Insert the remaining table definitions before the closing triple quotes in __create_tables():
            CREATE TABLE IF NOT EXISTS inventory_items (
                id INTEGER PRIMARY KEY,
                name TEXT NOT NULL,
                quantity INTEGER NOT NULL CHECK (quantity >= 0),
                location TEXT NOT NULL
            );

            CREATE TABLE IF NOT EXISTS inventory_tasks (
                id INTEGER PRIMARY KEY,
                item_id INTEGER NOT NULL,
                description TEXT NOT NULL,
                status TEXT NOT NULL DEFAULT 'Open'
                    CHECK (status IN ('Open', 'Done')),
                FOREIGN KEY (item_id)
                    REFERENCES inventory_items(id)
                    ON DELETE CASCADE
            );

How are the tables related?

The inventory_items table stores each item's name. It also stores its quantity and location.

The inventory_tasks.item_id column points to inventory_items.id. The ON DELETE CASCADE rule removes dependent tasks when their inventory item is deleted.

  • Save database.py.
  • Confirm that database.py appears beside models.py in the Explorer sidebar.

Database file in the wrong place?

Check that database.py sits directly inside company-inventory-manager. A nested copy will not be imported by the application.

Ask for help: Help me check whether database.py is saved inside my company-inventory-manager folder.

The schema runs when the application creates DatabaseManager. Connecting it now gives you visible proof that the local database exists.

  • Add this import below the Tkinter imports in myscript.py:
from database import DatabaseManager

What does this import provide?

The import gives the interface access to your database class. This keeps storage responsibilities outside the GUI code.

  • Add these lines inside InventoryApp.__init__() after the window size configuration:
        self.database = DatabaseManager()
        self.root.protocol("WM_DELETE_WINDOW", self.close)

What happens when the app starts?

  • The DatabaseManager() call opens company_inventory.db. SQLite creates the file when it does not already exist.
  • The window protocol directs the close action to your own close() method.
  • Add this method near the bottom of the InventoryApp class:
    def close(self) -> None:
        self.database.close()
        self.root.destroy()

Why close the connection?

The close() method releases the active database connection before destroying the window. This gives the application one clean shutdown path.

  • Save myscript.py.

Before you launch the application, which new file do you expect SQLite to create?

  • Launch the application from the integrated terminal by running:
python myscript.py

What does this run prove?

Creating DatabaseManager opens the database connection. Its constructor also creates all three tables.

  • Close the application window.
  • Refresh the Visual Studio Code Explorer sidebar.
  • Confirm that company_inventory.db appears inside company-inventory-manager.

Your application now creates a local relational database. The next code gives the dashboard a safe path for writing and reading inventory.

Add parameterized inventory operations

Parameterized SQL keeps user values separate from the SQL statement. Each ? placeholder receives one value from the parameter tuple.

  • Add add_inventory_item() below __create_tables() in database.py:
    def add_inventory_item(
        self,
        name: str,
        quantity: int,
        location: str,
    ) -> int:
        with self.__connection:
            cursor = self.__connection.execute(
                """
                INSERT INTO inventory_items (name, quantity, location)
                VALUES (?, ?, ?)
                """,
                (name.strip(), quantity, location.strip()),
            )
        return cursor.lastrowid

How does the write operation work?

  • The connection context commits the insertion when the operation succeeds.
  • The tuple binds the cleaned name. It also binds the quantity and cleaned location.
  • The returned lastrowid identifies the new inventory record.
  • Save database.py.
  • Check the new method for syntax errors by running the application:
python myscript.py

What should you see?

You should see the existing inventory window open. This confirms that Python can load the new database method.

  • Close the application window.
  • Add list_inventory_items() below add_inventory_item() in database.py:
    def list_inventory_items(self) -> list[InventoryItem]:
        rows = self.__connection.execute(
            """
            SELECT id, name, quantity, location
            FROM inventory_items
            ORDER BY name
            """
        ).fetchall()
        return [
            InventoryItem(
                row["id"],
                row["name"],
                row["quantity"],
                row["location"],
            )
            for row in rows
        ]

How does the read operation work?

  • The query reads every inventory row in name order.
  • The sqlite3.Row factory exposes each value through names such as row["quantity"].
  • The list comprehension converts database rows into InventoryItem objects.
  • Add the database shutdown method below list_inventory_items():
    def close(self) -> None:
        self.__connection.close()

What does this method release?

The method closes the private SQLite connection. The interface calls it when the application window closes.

  • Save database.py.
  • Confirm that the completed database class loads by running the application:
python myscript.py

What does this check confirm?

You should see the inventory window open without a syntax error. The database class now provides the write and read operations needed by the dashboard.

  • Close the application window.

Application no longer opening?

  • Check that each database method is indented inside DatabaseManager.
  • Check that the three values tuple follows the SQL string inside add_inventory_item().
  • Check that list_inventory_items() returns InventoryItem objects from models.py.

Ask for help: Help me debug why DatabaseManager prevents my Tkinter application from opening.

✔️ Awesome, I've got everything!

Your database class now creates the relational schema. It also saves and reloads inventory records.

ⓧ I'd like to double check the full code

Compare your completed database.py file with this reference.

import sqlite3

from models import InventoryItem, InventoryTask


class DatabaseManager:
    def __init__(self, database_name: str = "company_inventory.db"):
        self.__connection = sqlite3.connect(database_name)
        self.__connection.row_factory = sqlite3.Row
        self.__connection.execute("PRAGMA foreign_keys = ON")
        self.__create_tables()

    def __create_tables(self) -> None:
        self.__connection.executescript(
            """
            CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY,
                username TEXT NOT NULL UNIQUE,
                salt BLOB NOT NULL,
                password_hash BLOB NOT NULL
            );

            CREATE TABLE IF NOT EXISTS inventory_items (
                id INTEGER PRIMARY KEY,
                name TEXT NOT NULL,
                quantity INTEGER NOT NULL CHECK (quantity >= 0),
                location TEXT NOT NULL
            );

            CREATE TABLE IF NOT EXISTS inventory_tasks (
                id INTEGER PRIMARY KEY,
                item_id INTEGER NOT NULL,
                description TEXT NOT NULL,
                status TEXT NOT NULL DEFAULT 'Open'
                    CHECK (status IN ('Open', 'Done')),
                FOREIGN KEY (item_id)
                    REFERENCES inventory_items(id)
                    ON DELETE CASCADE
            );
            """
        )

    def add_inventory_item(
        self,
        name: str,
        quantity: int,
        location: str,
    ) -> int:
        with self.__connection:
            cursor = self.__connection.execute(
                """
                INSERT INTO inventory_items (name, quantity, location)
                VALUES (?, ?, ?)
                """,
                (name.strip(), quantity, location.strip()),
            )
        return cursor.lastrowid

    def list_inventory_items(self) -> list[InventoryItem]:
        rows = self.__connection.execute(
            """
            SELECT id, name, quantity, location
            FROM inventory_items
            ORDER BY name
            """
        ).fetchall()
        return [
            InventoryItem(
                row["id"],
                row["name"],
                row["quantity"],
                row["location"],
            )
            for row in rows
        ]

    def close(self) -> None:
        self.__connection.close()
Connect the inventory dashboard

The dashboard still writes to the temporary Python list. Replacing that path with DatabaseManager makes the existing form persistent.

  • Locate the method connected to the Add Item button in myscript.py.
  • Remove the statement that appends the new item to the in-memory list.
  • Add the validated item through the database with this line:
        self.database.add_inventory_item(name, quantity, location)

Where does the item go now?

The form sends the cleaned name. It also sends the validated quantity and location to add_inventory_item().

  • Replace the prototype's list-based refresh method with this database-backed version:
    def refresh_data(self) -> None:
        items = self.database.list_inventory_items()

        for row_id in self.inventory_tree.get_children():
            self.inventory_tree.delete(row_id)
        for item in items:
            self.inventory_tree.insert(
                "",
                "end",
                values=(
                    item.record_id,
                    item.title,
                    item.quantity,
                    item.location,
                ),
            )

How does the dashboard reload?

  • The method retrieves fresh InventoryItem objects from the database.
  • It clears the current Treeview rows before inserting the latest database records.
  • Each row displays the stored ID. It also displays the item's title, quantity, and location.
  • Remove the in-memory inventory list from InventoryApp.__init__().
  • Save myscript.py.
  • Launch the database-backed dashboard by running:
python myscript.py

What is running now?

The same inventory form now reads its rows from company_inventory.db. New entries use the parameterized insertion method.

  • Enter Monitor in the Name field.
  • Enter 4 in the Quantity field.
  • Enter Warehouse A in the Location field.
  • Click Add Item.

You should see a row for Monitor with quantity 4 and location Warehouse A.

  • Close the application window.

Before you reopen the application, do you expect the Monitor row to return?

  • Reopen the application from the integrated terminal by running:
python myscript.py

What proves persistence?

You should see the Monitor row load again. The record survived the restart because it came from company_inventory.db.

Item missing after restart?

  • Check that the add handler calls self.database.add_inventory_item(name, quantity, location).
  • Check that refresh_data() reads from self.database.list_inventory_items().
  • Check that company_inventory.db remains inside company-inventory-manager.

Ask for help: Help me find why my inventory item disappears after restarting the application.

✔️ Awesome, I've got everything!

Your dashboard now saves inventory to SQLite. Double check that you saved myscript.py before continuing.

ⓧ I'd like to double check the full code

Compare your database-backed inventory application with this reference.

import tkinter as tk
from tkinter import messagebox, ttk

from database import DatabaseManager


class InventoryApp:
    def __init__(self, root: tk.Tk):
        self.root = root
        self.root.title("Company Inventory Manager")
        self.root.geometry("900x620")
        self.root.minsize(760, 520)
        self.database = DatabaseManager()
        self.root.protocol("WM_DELETE_WINDOW", self.close)

        notebook = ttk.Notebook(self.root)
        notebook.pack(padx=12, pady=(0, 12), fill="both", expand=True)

        inventory_tab = ttk.Frame(notebook, padding=12)
        notebook.add(inventory_tab, text="Inventory")

        self.build_inventory_tab(inventory_tab)
        self.refresh_data()

    def build_inventory_tab(self, parent: ttk.Frame) -> None:
        form = ttk.LabelFrame(parent, text="Add Inventory Item", padding=10)
        form.pack(fill="x", pady=(0, 12))

        self.item_name = tk.StringVar()
        self.item_quantity = tk.StringVar()
        self.item_location = tk.StringVar()
        for column in range(4):
            form.columnconfigure(column, weight=1)

        ttk.Label(form, text="Name").grid(row=0, column=0, sticky="w")
        ttk.Label(form, text="Quantity").grid(row=0, column=1, sticky="w")
        ttk.Label(form, text="Location").grid(row=0, column=2, sticky="w")
        ttk.Entry(form, textvariable=self.item_name).grid(
            row=1, column=0, padx=(0, 8), sticky="ew"
        )
        ttk.Entry(form, textvariable=self.item_quantity).grid(
            row=1, column=1, padx=(0, 8), sticky="ew"
        )
        ttk.Entry(form, textvariable=self.item_location).grid(
            row=1, column=2, padx=(0, 8), sticky="ew"
        )
        ttk.Button(form, text="Add Item", command=self.add_item).grid(
            row=1, column=3, sticky="ew"
        )

        columns = ("id", "name", "quantity", "location")
        self.inventory_tree = ttk.Treeview(
            parent,
            columns=columns,
            show="headings",
            height=13,
        )
        for column, heading in zip(
            columns,
            ("ID", "Name", "Quantity", "Location"),
        ):
            self.inventory_tree.heading(column, text=heading)
        self.inventory_tree.column("id", width=60, anchor="center")
        self.inventory_tree.column("quantity", width=100, anchor="center")
        self.inventory_tree.pack(fill="both", expand=True)

    def add_item(self) -> None:
        name = self.item_name.get().strip()
        location = self.item_location.get().strip()
        try:
            quantity = int(self.item_quantity.get())
        except ValueError:
            quantity = -1

        if not name or not location or quantity < 0:
            messagebox.showerror(
                "Invalid Item",
                "Enter a name, a non-negative whole quantity, and a location.",
            )
            return

        self.database.add_inventory_item(name, quantity, location)
        self.item_name.set("")
        self.item_quantity.set("")
        self.item_location.set("")
        self.refresh_data()

    def refresh_data(self) -> None:
        items = self.database.list_inventory_items()

        for row_id in self.inventory_tree.get_children():
            self.inventory_tree.delete(row_id)
        for item in items:
            self.inventory_tree.insert(
                "",
                "end",
                values=(
                    item.record_id,
                    item.title,
                    item.quantity,
                    item.location,
                ),
            )

    def close(self) -> None:
        self.database.close()
        self.root.destroy()


if __name__ == "__main__":
    root = tk.Tk()
    InventoryApp(root)
    root.mainloop()

That restart failure is fixed. Next, you'll protect the persistent dashboard so company records only appear after a verified login.

Add Account Login Protection

The SQLite database now preserves your inventory between sessions. Anyone who launches the application can still reach those company records.

This step adds an application-level identity gate. Account registration stores salted password hashes. A successful login becomes the only path to the persistent inventory dashboard.

In this step, get ready to:
  • Store salted password hashes for registered accounts.
  • Build Login and Create Account tabs with masked password fields.
  • Require a verified login before displaying the inventory dashboard.
Store salted password hashes

Password hashing converts a password into a derived value for storage. PBKDF2 combines the password with a random salt. The application repeats that calculation during login to verify an attempt.

What does this login protect?

The account gate controls access through Company Inventory Manager. Standard SQLite files do not provide server-style user accounts.

A person with direct access to company_inventory.db can still copy the file. The login screen protects the application's dashboard workflow.

  • Switch back to database.py in Visual Studio Code.
  • Replace the import section with this expanded group:
import hashlib
import hmac
import secrets
import sqlite3

from models import InventoryItem, InventoryTask


PBKDF2_ITERATIONS = 500_000

Why are these imports needed?

  • The hashlib module derives the password hash.
  • The secrets module generates a random salt for each account.
  • The hmac module compares the stored hash with the attempted hash.
  • The PBKDF2_ITERATIONS constant keeps the calculation setting in one place.
  • Find the end of __create_tables() inside DatabaseManager.
  • Add the private hashing helper below it:
    @staticmethod
    def __hash_password(password: str, salt: bytes) -> bytes:
        return hashlib.pbkdf2_hmac(
            'sha256',
            password.encode(),
            salt,
            PBKDF2_ITERATIONS,
        )

What does this helper do?

  • The helper converts the entered password to bytes.
  • The hashlib.pbkdf2_hmac() call combines those bytes with the account's salt.
  • The same password produces a different stored value when its salt changes.
  • Save database.py.
  • Check that the existing application still starts by running:
python myscript.py

What does this check prove?

You should see the existing inventory dashboard with its saved rows. This confirms that the new imports and hashing helper did not interrupt database startup.

  • Close the application window.
  • Return to database.py.
  • Add the registration method below __hash_password():
    def register_user(self, username: str, password: str) -> bool:
        salt = secrets.token_bytes(16)
        password_hash = self.__hash_password(password, salt)
        try:
            with self.__connection:
                self.__connection.execute(
                    """
                    INSERT INTO users (username, salt, password_hash)
                    VALUES (?, ?, ?)
                    """,
                    (username.strip(), salt, password_hash),
                )
        except sqlite3.IntegrityError:
            return False
        return True

How does registration protect passwords?

  • The method creates a fresh 16-byte salt for each registration.
  • The parameterized INSERT stores the username.
  • The same INSERT stores the salt and derived hash.
  • The sqlite3.IntegrityError handler reports a duplicate username without replacing the existing account.
  • Add the authentication method below register_user():
    def authenticate_user(self, username: str, password: str) -> bool:
        row = self.__connection.execute(
            """
            SELECT salt, password_hash
            FROM users
            WHERE username = ?
            """,
            (username.strip(),),
        ).fetchone()
        if row is None:
            return False

        attempted_hash = self.__hash_password(password, row["salt"])
        return hmac.compare_digest(attempted_hash, row["password_hash"])

How does authentication work?

  • The query retrieves one account's stored salt and hash.
  • The helper derives a new hash from the attempted password.
  • The hmac.compare_digest() call checks whether both derived values match.
  • A missing username returns False without opening the dashboard.
  • Save database.py.
  • Confirm the database changes still allow the application to start by running:
python myscript.py

What should you see?

You should see the existing inventory dashboard with your persisted items. The database can now register accounts and verify login attempts.

  • Close the application window.

Application no longer starting?

  • Check that PBKDF2_ITERATIONS appears above DatabaseManager.
  • Check that both account methods use four spaces of indentation inside DatabaseManager.
  • Check that each SQL placeholder has a matching parameter value.

Ask for help: Help me debug my account storage methods.

Build the authentication screen

The database now has the account logic. The Tkinter interface needs to collect credentials before it constructs the inventory dashboard.

  • Switch back to myscript.py.
  • Add the registration handler inside InventoryApp:
    def register(self) -> None:
        username = self.register_username.get().strip()
        password = self.register_password.get()
        confirmation = self.register_confirm.get()

        if not username or len(password) < 8:
            messagebox.showerror(
                "Invalid Account",
                "Enter a username and a password with at least 8 characters.",
            )
            return
        if password != confirmation:
            messagebox.showerror("Invalid Account", "Passwords do not match.")
            return
        if not self.database.register_user(username, password):
            messagebox.showerror("Invalid Account", "That username already exists.")
            return

        messagebox.showinfo("Account Created", "You can now use the Login tab.")
        self.register_username.set("")
        self.register_password.set("")
        self.register_confirm.set("")

What does registration validate?

  • The username must contain a value.
  • The project requires a password with at least eight characters.
  • The confirmation must match the password.
  • A duplicate username produces an error message.
  • Add the login handler below register():
    def login(self) -> None:
        username = self.login_username.get().strip()
        password = self.login_password.get()
        if self.database.authenticate_user(username, password):
            self.show_dashboard(username)
        else:
            messagebox.showerror("Login Failed", "Incorrect username or password.")

What happens during login?

A valid attempt calls show_dashboard() with the verified username. A failed attempt displays an error while the authentication screen stays in place.

  • Save myscript.py.
  • Confirm the new handlers contain valid Python by running:
python myscript.py

What does this run confirm?

You should still see the existing inventory dashboard. This confirms that both handlers load without a syntax error before you connect them to the interface.

  • Close the application window.
  • Add these window-switching methods inside InventoryApp:
    def clear_window(self) -> None:
        for child in self.root.winfo_children():
            child.destroy()

    def show_authentication(self) -> None:
        self.clear_window()
        ttk.Label(
            self.root,
            text="Company Inventory Manager",
            font=("Segoe UI", 18, "bold"),
        ).pack(pady=(30, 15))

        notebook = ttk.Notebook(self.root)
        notebook.pack(padx=40, pady=10, fill="both", expand=True)

        login_tab = ttk.Frame(notebook, padding=25)
        register_tab = ttk.Frame(notebook, padding=25)
        notebook.add(login_tab, text="Login")
        notebook.add(register_tab, text="Create Account")

How does screen switching work?

  • The clear_window() method removes every widget from the current view.
  • The show_authentication() method creates separate account tabs.
  • Rebuilding the window lets login and logout switch between views.
  • Find InventoryApp.__init__() near the top of the class.
  • Update the method so it matches this version:
    def __init__(self, root: tk.Tk):
        self.root = root
        self.root.title("Company Inventory Manager")
        self.root.geometry("900x620")
        self.root.minsize(760, 520)
        self.database = DatabaseManager()
        self.root.protocol("WM_DELETE_WINDOW", self.close)
        self.show_authentication()

What changes at startup?

The constructor now calls show_authentication() when the application launches. The dashboard is no longer built during startup.

  • Save myscript.py.
  • Check the first protected view by running:
python myscript.py

What should open first?

You should see the Company Inventory Manager heading above empty Login and Create Account tabs. This proves that the application now starts at the account gate.

  • Close the application window.
  • Return to show_authentication().
  • Add these Login widgets below the two notebook.add() calls:
        self.login_username = tk.StringVar()
        self.login_password = tk.StringVar()
        ttk.Label(login_tab, text="Username").grid(row=0, column=0, sticky="w")
        ttk.Entry(login_tab, textvariable=self.login_username, width=32).grid(
            row=1, column=0, pady=(4, 14), sticky="ew"
        )
        ttk.Label(login_tab, text="Password").grid(row=2, column=0, sticky="w")
        tk.Entry(
            login_tab,
            textvariable=self.login_password,
            show="*",
            width=32,
        ).grid(row=3, column=0, pady=(4, 14), sticky="ew")
        ttk.Button(login_tab, text="Login", command=self.login).grid(
            row=4, column=0, sticky="ew"
        )

How is the Login tab connected?

  • The two StringVar objects hold the current input values.
  • The password entry uses show="*" to mask displayed characters.
  • The Login button calls login().
  • Add these Create Account widgets below the Login widgets:
        self.register_username = tk.StringVar()
        self.register_password = tk.StringVar()
        self.register_confirm = tk.StringVar()
        ttk.Label(register_tab, text="Username").grid(row=0, column=0, sticky="w")
        ttk.Entry(
            register_tab,
            textvariable=self.register_username,
            width=32,
        ).grid(row=1, column=0, pady=(4, 10), sticky="ew")
        ttk.Label(register_tab, text="Password").grid(row=2, column=0, sticky="w")
        tk.Entry(
            register_tab,
            textvariable=self.register_password,
            show="*",
            width=32,
        ).grid(row=3, column=0, pady=(4, 10), sticky="ew")
        ttk.Label(register_tab, text="Confirm Password").grid(
            row=4, column=0, sticky="w"
        )
        tk.Entry(
            register_tab,
            textvariable=self.register_confirm,
            show="*",
            width=32,
        ).grid(row=5, column=0, pady=(4, 14), sticky="ew")
        ttk.Button(
            register_tab,
            text="Create Account",
            command=self.register,
        ).grid(row=6, column=0, sticky="ew")

How does the account form work?

  • Three separate variables hold the username and both password entries.
  • Both password fields mask their displayed characters.
  • The Create Account button calls register().
  • Save myscript.py.
  • Confirm that both account forms render by running:
python myscript.py

What should you see now?

The Login tab should contain username and password fields. The Create Account tab should contain username, password, and confirmation fields.

Each password field should display asterisks while you type. The inventory dashboard should remain hidden.

  • Close the application window.
  • Add the protected dashboard method below login():
    def show_dashboard(self, username: str) -> None:
        self.clear_window()
        header = ttk.Frame(self.root, padding=12)
        header.pack(fill="x")
        ttk.Label(
            header,
            text=f"Signed in as {username}",
            font=("Segoe UI", 12, "bold"),
        ).pack(side="left")
        ttk.Button(
            header,
            text="Log Out",
            command=self.show_authentication,
        ).pack(side="right")

        notebook = ttk.Notebook(self.root)
        notebook.pack(padx=12, pady=(0, 12), fill="both", expand=True)

        inventory_tab = ttk.Frame(notebook, padding=12)
        notebook.add(inventory_tab, text="Inventory")

        self.build_inventory_tab(inventory_tab)
        self.refresh_data()

How does the dashboard stay protected?

  • The method receives the verified username from login().
  • The inventory interface is constructed only when this method runs.
  • The Log Out button clears the dashboard.
  • The same button returns the window to show_authentication().
  • Save myscript.py.

Authentication screen not loading?

  • Check that each new method uses four spaces of indentation inside InventoryApp.
  • Check that widget code inside each method uses eight spaces of indentation.
  • Confirm that messagebox remains in the tkinter import.

Ask for help: Help me debug my authentication screen.

✔️ Awesome, I've got everything!

Good work. Double check that you saved database.py and myscript.py.

ⓧ I'd like to double check the full code

Compare these cumulative files with yours. Leave models.py unchanged.

import hashlib
import hmac
import secrets
import sqlite3

from models import InventoryItem, InventoryTask


PBKDF2_ITERATIONS = 500_000


class DatabaseManager:
    def __init__(self, database_name: str = "company_inventory.db"):
        self.__connection = sqlite3.connect(database_name)
        self.__connection.row_factory = sqlite3.Row
        self.__connection.execute("PRAGMA foreign_keys = ON")
        self.__create_tables()

    def __create_tables(self) -> None:
        self.__connection.executescript(
            """
            CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY,
                username TEXT NOT NULL UNIQUE,
                salt BLOB NOT NULL,
                password_hash BLOB NOT NULL
            );

            CREATE TABLE IF NOT EXISTS inventory_items (
                id INTEGER PRIMARY KEY,
                name TEXT NOT NULL,
                quantity INTEGER NOT NULL CHECK (quantity >= 0),
                location TEXT NOT NULL
            );

            CREATE TABLE IF NOT EXISTS inventory_tasks (
                id INTEGER PRIMARY KEY,
                item_id INTEGER NOT NULL,
                description TEXT NOT NULL,
                status TEXT NOT NULL DEFAULT 'Open'
                    CHECK (status IN ('Open', 'Done')),
                FOREIGN KEY (item_id)
                    REFERENCES inventory_items(id)
                    ON DELETE CASCADE
            );
            """
        )

    @staticmethod
    def __hash_password(password: str, salt: bytes) -> bytes:
        return hashlib.pbkdf2_hmac(
            'sha256',
            password.encode(),
            salt,
            PBKDF2_ITERATIONS,
        )

    def register_user(self, username: str, password: str) -> bool:
        salt = secrets.token_bytes(16)
        password_hash = self.__hash_password(password, salt)
        try:
            with self.__connection:
                self.__connection.execute(
                    """
                    INSERT INTO users (username, salt, password_hash)
                    VALUES (?, ?, ?)
                    """,
                    (username.strip(), salt, password_hash),
                )
        except sqlite3.IntegrityError:
            return False
        return True

    def authenticate_user(self, username: str, password: str) -> bool:
        row = self.__connection.execute(
            """
            SELECT salt, password_hash
            FROM users
            WHERE username = ?
            """,
            (username.strip(),),
        ).fetchone()
        if row is None:
            return False

        attempted_hash = self.__hash_password(password, row["salt"])
        return hmac.compare_digest(attempted_hash, row["password_hash"])

    def add_inventory_item(
        self,
        name: str,
        quantity: int,
        location: str,
    ) -> int:
        with self.__connection:
            cursor = self.__connection.execute(
                """
                INSERT INTO inventory_items (name, quantity, location)
                VALUES (?, ?, ?)
                """,
                (name.strip(), quantity, location.strip()),
            )
        return cursor.lastrowid

    def list_inventory_items(self) -> list[InventoryItem]:
        rows = self.__connection.execute(
            """
            SELECT id, name, quantity, location
            FROM inventory_items
            ORDER BY name
            """
        ).fetchall()
        return [
            InventoryItem(
                row["id"],
                row["name"],
                row["quantity"],
                row["location"],
            )
            for row in rows
        ]

    def add_task(self, item_id: int, description: str) -> int:
        with self.__connection:
            cursor = self.__connection.execute(
                """
                INSERT INTO inventory_tasks (item_id, description)
                VALUES (?, ?)
                """,
                (item_id, description.strip()),
            )
        return cursor.lastrowid

    def list_tasks(self) -> list[InventoryTask]:
        rows = self.__connection.execute(
            """
            SELECT
                inventory_tasks.id,
                inventory_tasks.item_id,
                inventory_tasks.description,
                inventory_tasks.status,
                inventory_items.name AS item_name
            FROM inventory_tasks
            JOIN inventory_items
                ON inventory_items.id = inventory_tasks.item_id
            ORDER BY inventory_tasks.id DESC
            """
        ).fetchall()
        return [
            InventoryTask(
                row["id"],
                row["description"],
                row["item_id"],
                row["item_name"],
                row["status"],
            )
            for row in rows
        ]

    def complete_task(self, task_id: int) -> None:
        with self.__connection:
            self.__connection.execute(
                """
                UPDATE inventory_tasks
                SET status = 'Done'
                WHERE id = ?
                """,
                (task_id,),
            )

    def close(self) -> None:
        self.__connection.close()
import tkinter as tk
from tkinter import messagebox, ttk

from database import DatabaseManager


class InventoryApp:
    def __init__(self, root: tk.Tk):
        self.root = root
        self.root.title("Company Inventory Manager")
        self.root.geometry("900x620")
        self.root.minsize(760, 520)
        self.database = DatabaseManager()
        self.root.protocol("WM_DELETE_WINDOW", self.close)
        self.show_authentication()

    def clear_window(self) -> None:
        for child in self.root.winfo_children():
            child.destroy()

    def show_authentication(self) -> None:
        self.clear_window()
        ttk.Label(
            self.root,
            text="Company Inventory Manager",
            font=("Segoe UI", 18, "bold"),
        ).pack(pady=(30, 15))

        notebook = ttk.Notebook(self.root)
        notebook.pack(padx=40, pady=10, fill="both", expand=True)

        login_tab = ttk.Frame(notebook, padding=25)
        register_tab = ttk.Frame(notebook, padding=25)
        notebook.add(login_tab, text="Login")
        notebook.add(register_tab, text="Create Account")

        self.login_username = tk.StringVar()
        self.login_password = tk.StringVar()
        ttk.Label(login_tab, text="Username").grid(row=0, column=0, sticky="w")
        ttk.Entry(login_tab, textvariable=self.login_username, width=32).grid(
            row=1, column=0, pady=(4, 14), sticky="ew"
        )
        ttk.Label(login_tab, text="Password").grid(row=2, column=0, sticky="w")
        tk.Entry(
            login_tab,
            textvariable=self.login_password,
            show="*",
            width=32,
        ).grid(row=3, column=0, pady=(4, 14), sticky="ew")
        ttk.Button(login_tab, text="Login", command=self.login).grid(
            row=4, column=0, sticky="ew"
        )

        self.register_username = tk.StringVar()
        self.register_password = tk.StringVar()
        self.register_confirm = tk.StringVar()
        ttk.Label(register_tab, text="Username").grid(row=0, column=0, sticky="w")
        ttk.Entry(
            register_tab,
            textvariable=self.register_username,
            width=32,
        ).grid(row=1, column=0, pady=(4, 10), sticky="ew")
        ttk.Label(register_tab, text="Password").grid(row=2, column=0, sticky="w")
        tk.Entry(
            register_tab,
            textvariable=self.register_password,
            show="*",
            width=32,
        ).grid(row=3, column=0, pady=(4, 10), sticky="ew")
        ttk.Label(register_tab, text="Confirm Password").grid(
            row=4, column=0, sticky="w"
        )
        tk.Entry(
            register_tab,
            textvariable=self.register_confirm,
            show="*",
            width=32,
        ).grid(row=5, column=0, pady=(4, 14), sticky="ew")
        ttk.Button(
            register_tab,
            text="Create Account",
            command=self.register,
        ).grid(row=6, column=0, sticky="ew")

    def register(self) -> None:
        username = self.register_username.get().strip()
        password = self.register_password.get()
        confirmation = self.register_confirm.get()

        if not username or len(password) < 8:
            messagebox.showerror(
                "Invalid Account",
                "Enter a username and a password with at least 8 characters.",
            )
            return
        if password != confirmation:
            messagebox.showerror("Invalid Account", "Passwords do not match.")
            return
        if not self.database.register_user(username, password):
            messagebox.showerror("Invalid Account", "That username already exists.")
            return

        messagebox.showinfo("Account Created", "You can now use the Login tab.")
        self.register_username.set("")
        self.register_password.set("")
        self.register_confirm.set("")

    def login(self) -> None:
        username = self.login_username.get().strip()
        password = self.login_password.get()
        if self.database.authenticate_user(username, password):
            self.show_dashboard(username)
        else:
            messagebox.showerror("Login Failed", "Incorrect username or password.")

    def show_dashboard(self, username: str) -> None:
        self.clear_window()
        header = ttk.Frame(self.root, padding=12)
        header.pack(fill="x")
        ttk.Label(
            header,
            text=f"Signed in as {username}",
            font=("Segoe UI", 12, "bold"),
        ).pack(side="left")
        ttk.Button(
            header,
            text="Log Out",
            command=self.show_authentication,
        ).pack(side="right")

        notebook = ttk.Notebook(self.root)
        notebook.pack(padx=12, pady=(0, 12), fill="both", expand=True)

        inventory_tab = ttk.Frame(notebook, padding=12)
        notebook.add(inventory_tab, text="Inventory")

        self.build_inventory_tab(inventory_tab)
        self.refresh_data()

    def build_inventory_tab(self, parent: ttk.Frame) -> None:
        form = ttk.LabelFrame(parent, text="Add Inventory Item", padding=10)
        form.pack(fill="x", pady=(0, 12))

        self.item_name = tk.StringVar()
        self.item_quantity = tk.StringVar()
        self.item_location = tk.StringVar()
        for column in range(4):
            form.columnconfigure(column, weight=1)

        ttk.Label(form, text="Name").grid(row=0, column=0, sticky="w")
        ttk.Label(form, text="Quantity").grid(row=0, column=1, sticky="w")
        ttk.Label(form, text="Location").grid(row=0, column=2, sticky="w")
        ttk.Entry(form, textvariable=self.item_name).grid(
            row=1, column=0, padx=(0, 8), sticky="ew"
        )
        ttk.Entry(form, textvariable=self.item_quantity).grid(
            row=1, column=1, padx=(0, 8), sticky="ew"
        )
        ttk.Entry(form, textvariable=self.item_location).grid(
            row=1, column=2, padx=(0, 8), sticky="ew"
        )
        ttk.Button(form, text="Add Item", command=self.add_item).grid(
            row=1, column=3, sticky="ew"
        )

        columns = ("id", "name", "quantity", "location")
        self.inventory_tree = ttk.Treeview(
            parent,
            columns=columns,
            show="headings",
            height=13,
        )
        for column, heading in zip(
            columns,
            ("ID", "Name", "Quantity", "Location"),
        ):
            self.inventory_tree.heading(column, text=heading)
        self.inventory_tree.column("id", width=60, anchor="center")
        self.inventory_tree.column("quantity", width=100, anchor="center")
        self.inventory_tree.pack(fill="both", expand=True)

    def add_item(self) -> None:
        name = self.item_name.get().strip()
        location = self.item_location.get().strip()
        try:
            quantity = int(self.item_quantity.get())
        except ValueError:
            quantity = -1

        if not name or not location or quantity < 0:
            messagebox.showerror(
                "Invalid Item",
                "Enter a name, a non-negative whole quantity, and a location.",
            )
            return

        self.database.add_inventory_item(name, quantity, location)
        self.item_name.set("")
        self.item_quantity.set("")
        self.item_location.set("")
        self.refresh_data()

    def refresh_data(self) -> None:
        items = self.database.list_inventory_items()

        for row_id in self.inventory_tree.get_children():
            self.inventory_tree.delete(row_id)
        for item in items:
            self.inventory_tree.insert(
                "",
                "end",
                values=(
                    item.record_id,
                    item.title,
                    item.quantity,
                    item.location,
                ),
            )

    def close(self) -> None:
        self.database.close()
        self.root.destroy()


if __name__ == "__main__":
    root = tk.Tk()
    InventoryApp(root)
    root.mainloop()
Verify the account gate

The final check proves both outcomes. A wrong password must leave the account screen visible. The correct password must reveal the saved inventory.

Before you run the application, which screen do you expect to see first?

  • Launch the protected application by running:
python myscript.py

What opens first?

You should see the Company Inventory Manager authentication screen. The Login and Create Account tabs should appear while the inventory dashboard remains hidden.

  • Select the Create Account tab.
  • Enter inventory_admin in the Username field.
  • Enter NextWork8 in the Password field.
  • Enter NextWork8 in the Confirm Password field.
  • Click Create Account.

You should see the Account Created message. The registration fields should clear after you dismiss it.

  • Return to the Login tab.
  • Enter inventory_admin in the Username field.
  • Enter WrongPass8 in the Password field.

Before you click Login, do you expect the dashboard to open with this password?

  • Click Login.

You should see the Incorrect username or password message. The Login tab should remain visible after you dismiss the error.

  • Replace the password with NextWork8.

Before you try again, what should a matching derived hash unlock?

  • Click Login.

You should now see Signed in as inventory_admin above the inventory dashboard. Your inventory rows from the previous step should still be present.

That is the gate working: an invalid attempt stays outside. A verified account reaches the persistent company records.

  • Click Log Out.

You should return to the Login and Create Account tabs. The dashboard now requires authentication for every application session.

Login behaving unexpectedly?

  • Create a different username if inventory_admin already exists from an earlier attempt.
  • Check that registration and login both apply .strip() to the username.
  • Check that authenticate_user() passes the stored salt into __hash_password().

Ask for help: Help me trace why my correct password does not open the dashboard.

Your persistent inventory now has an account gate. Next up, you will connect tasks to inventory items. You will also make both record types demonstrate polymorphism in the interface.

Link Tasks and Show Record Details

Your authentication gate now protects the persistent inventory dashboard. The next goal is to turn those stored items into work that your team can track through the Tkinter interface.

The existing SQLite methods already connect each task to an inventory item. This step brings that relationship into the dashboard and makes polymorphism visible through the shared get_details() method.

In this step, get ready to:
  • Build the Tasks and Record Details tabs.
  • Connect task controls to the relational database operations.
  • Display both record types through one polymorphic method call.
Build the task workspace

A ttk.Notebook keeps inventory management and task management in separate views. A third view presents descriptions from both object types without duplicating presentation logic.

  • Switch back to myscript.py in Visual Studio Code.
  • Find the show_dashboard() method.
  • Replace its current dashboard notebook section with this code:
        notebook = ttk.Notebook(self.root)
        notebook.pack(padx=12, pady=(0, 12), fill="both", expand=True)

        inventory_tab = ttk.Frame(notebook, padding=12)
        tasks_tab = ttk.Frame(notebook, padding=12)
        details_tab = ttk.Frame(notebook, padding=12)
        notebook.add(inventory_tab, text="Inventory")
        notebook.add(tasks_tab, text="Tasks")
        notebook.add(details_tab, text="Record Details")

        self.build_inventory_tab(inventory_tab)
        self.build_tasks_tab(tasks_tab)
        self.details_list = tk.Listbox(details_tab, font=("Consolas", 10))
        self.details_list.pack(fill="both", expand=True)
        self.refresh_data()

How does the dashboard wiring work?

  • The three frames give inventory records, task records, and object descriptions their own tabs.
  • The details_list widget provides one place to display descriptions from every record object.
  • The final refresh_data() call fills the dashboard whenever a user logs in.
  • Find the existing build_inventory_tab() method.
  • Add this new method below build_inventory_tab() to create the task form:
    def build_tasks_tab(self, parent: ttk.Frame) -> None:
        form = ttk.LabelFrame(parent, text="Add Inventory Task", padding=10)
        form.pack(fill="x", pady=(0, 12))
        form.columnconfigure(0, weight=1)
        form.columnconfigure(1, weight=2)

        self.task_item = tk.StringVar()
        self.task_description = tk.StringVar()
        ttk.Label(form, text="Inventory Item").grid(row=0, column=0, sticky="w")
        ttk.Label(form, text="Task Description").grid(row=0, column=1, sticky="w")
        self.task_item_combo = ttk.Combobox(
            form,
            textvariable=self.task_item,
            state="readonly",
        )
        self.task_item_combo.grid(row=1, column=0, padx=(0, 8), sticky="ew")

What does the task form do?

  • The item selector uses a readonly combobox so every task points to an inventory item that already exists.
  • The two StringVar objects carry the selected item label and the task description into the add action.
  • The column weights give the description field more room than the item selector.
  • Save myscript.py.

Before you run the application, predict which new dashboard tabs you expect to see after login.

  • Launch the application from the integrated terminal by running:
python myscript.py

What does this command check?

This starts the updated desktop application through the same entry point you used earlier. A successful launch proves the new dashboard references a valid build_tasks_tab() method.

  • Enter the username from your existing account in the Username field.
  • Enter your existing password in the Password field.
  • Click Login.
  • Select the Tasks tab.

You should see the Inventory, Tasks, and Record Details tabs. The Tasks tab should contain the Inventory Item selector and Task Description field.

Missing a dashboard tab?

  • Check that each notebook.add() line remains inside show_dashboard().
  • Check that build_tasks_tab() aligns with the other methods in the InventoryApp class.

Ask for help: Help me check why the Tasks or Record Details tab is missing from my Tkinter notebook.

  • Close the application window.
  • Return to the final self.task_item_combo.grid() line in build_tasks_tab().
  • Add the task entry field and task table directly below that line:
        ttk.Entry(form, textvariable=self.task_description).grid(
            row=1, column=1, padx=(0, 8), sticky="ew"
        )
        ttk.Button(form, text="Add Task", command=self.add_task).grid(
            row=1, column=2, sticky="ew"
        )

        columns = ("id", "item", "description", "status")
        self.task_tree = ttk.Treeview(
            parent,
            columns=columns,
            show="headings",
            height=11,
            selectmode="browse",
        )
        for column, heading in zip(
            columns,
            ("ID", "Item", "Description", "Status"),
        ):
            self.task_tree.heading(column, text=heading)
        self.task_tree.column("id", width=60, anchor="center")
        self.task_tree.column("status", width=100, anchor="center")
        self.task_tree.pack(fill="both", expand=True)
        ttk.Button(
            parent,
            text="Mark Selected Done",
            command=self.complete_selected_task,
        ).pack(pady=(10, 0), anchor="e")

How does the task table work?

  • The Treeview presents each task's database ID, related item, description, and status.
  • The browse selection mode keeps the completion action focused on one task at a time.
  • The Mark Selected Done button sends the selected row to complete_selected_task().
  • Save myscript.py.

Before you reopen the application, predict which four headings the new task table should contain.

  • Launch the updated application by running:
python myscript.py

What should this launch prove?

The application should open without a layout error. After login, the Tasks tab should have a table with ID, Item, Description, and Status headings.

  • Log in with your existing account.
  • Select the Tasks tab.

You should see the task form above an empty table. The Mark Selected Done button should sit below the table.

Task table missing?

  • Confirm the table code stays indented inside build_tasks_tab().
  • Confirm the button command uses self.complete_selected_task without parentheses.

Ask for help: Help me find why my task Treeview or Mark Selected Done button is missing.

  • Close the application window.
Load relational records

The dashboard needs one refresh cycle for inventory items and their related tasks. Each database row becomes an InventoryItem or InventoryTask object before the interface displays it.

  • Find the current refresh_data() method in myscript.py.
  • Replace that method with this inventory and selector refresh code:
    def refresh_data(self) -> None:
        items = self.database.list_inventory_items()
        tasks = self.database.list_tasks()

        for row_id in self.inventory_tree.get_children():
            self.inventory_tree.delete(row_id)
        for item in items:
            self.inventory_tree.insert(
                "",
                "end",
                values=(
                    item.record_id,
                    item.title,
                    item.quantity,
                    item.location,
                ),
            )

        self.item_lookup = {
            f"{item.record_id}: {item.title}": item.record_id for item in items
        }
        item_labels = list(self.item_lookup)
        self.task_item_combo["values"] = item_labels
        if item_labels and self.task_item.get() not in self.item_lookup:
            self.task_item.set(item_labels[0])
        if not item_labels:
            self.task_item.set("")

How does the item selector preserve the relationship?

  • The database methods return objects with their record IDs and display values already attached.
  • The item_lookup dictionary maps a readable label such as 1: Item Name to the corresponding database ID.
  • The readonly combobox receives those labels so task creation can store the correct item_id.
  • Place the cursor below self.task_item.set("").
  • Complete refresh_data() with this task and details refresh code:
        for row_id in self.task_tree.get_children():
            self.task_tree.delete(row_id)
        for task in tasks:
            self.task_tree.insert(
                "",
                "end",
                values=(
                    task.record_id,
                    task.item_name,
                    task.title,
                    task.status,
                ),
            )

        self.details_list.delete(0, tk.END)
        records = [*items, *tasks]
        for record in records:
            self.details_list.insert(tk.END, record.get_details())

Where does polymorphism happen?

The records collection contains both inventory items and tasks. The loop calls record.get_details() without checking which subclass each object uses.

Python selects the overridden implementation attached to each object. One loop therefore produces the correct description for both record types.

  • Save myscript.py.

Before you run the application, predict how an existing inventory item's database ID will appear inside the task selector.

  • Launch the application by running:
python myscript.py

What does this refresh check prove?

A successful launch proves the dashboard can load inventory objects and task objects in the same refresh cycle. The item selector should use labels that combine each inventory ID with its title.

  • Log in with your existing account.
  • Select the Tasks tab.

You should see your persistent inventory items in the selector. Each option should follow the ID and title format shown by 1: Item Name.

Item selector empty?

  • Confirm the Inventory tab still contains at least one saved item.
  • Check that self.task_item_combo["values"] receives item_labels.
  • Check that refresh_data() runs after the dashboard widgets are created.

Ask for help: Help me debug why my readonly task item selector is empty even though inventory rows exist.

  • Close the application window.
  • Find the end of the existing add_item() method.
  • Add these task action methods directly below add_item().
    def add_task(self) -> None:
        description = self.task_description.get().strip()
        item_id = self.item_lookup.get(self.task_item.get())
        if item_id is None or not description:
            messagebox.showerror(
                "Invalid Task",
                "Select an inventory item and enter a task description.",
            )
            return

        self.database.add_task(item_id, description)
        self.task_description.set("")
        self.refresh_data()

    def complete_selected_task(self) -> None:
        selected = self.task_tree.selection()
        if not selected:
            messagebox.showwarning("No Task", "Select a task first.")
            return
        task_id = int(self.task_tree.item(selected[0], "values")[0])
        self.database.complete_task(task_id)
        self.refresh_data()

How do the task actions use relational IDs?

  • The add action translates the selected label back into its inventory ID before calling add_task() on the database manager.
  • The completion action reads the selected task ID from the first table column before calling complete_task().
  • Both actions call refresh_data() so the selector, tables, and details view stay synchronized.
  • Save myscript.py.

✔️ Awesome, I've got everything!

Great. Double check that myscript.py is saved before testing the complete workflow.

ⓧ I'd like to double check the full code

Compare your complete myscript.py file with this reference.

import tkinter as tk
from tkinter import messagebox, ttk

from database import DatabaseManager


class InventoryApp:
    def __init__(self, root: tk.Tk):
        self.root = root
        self.root.title("Company Inventory Manager")
        self.root.geometry("900x620")
        self.root.minsize(760, 520)
        self.database = DatabaseManager()
        self.root.protocol("WM_DELETE_WINDOW", self.close)
        self.show_authentication()

    def clear_window(self) -> None:
        for child in self.root.winfo_children():
            child.destroy()

    def show_authentication(self) -> None:
        self.clear_window()
        ttk.Label(
            self.root,
            text="Company Inventory Manager",
            font=("Segoe UI", 18, "bold"),
        ).pack(pady=(30, 15))

        notebook = ttk.Notebook(self.root)
        notebook.pack(padx=40, pady=10, fill="both", expand=True)

        login_tab = ttk.Frame(notebook, padding=25)
        register_tab = ttk.Frame(notebook, padding=25)
        notebook.add(login_tab, text="Login")
        notebook.add(register_tab, text="Create Account")

        self.login_username = tk.StringVar()
        self.login_password = tk.StringVar()
        ttk.Label(login_tab, text="Username").grid(row=0, column=0, sticky="w")
        ttk.Entry(login_tab, textvariable=self.login_username, width=32).grid(
            row=1, column=0, pady=(4, 14), sticky="ew"
        )
        ttk.Label(login_tab, text="Password").grid(row=2, column=0, sticky="w")
        tk.Entry(
            login_tab,
            textvariable=self.login_password,
            show="*",
            width=32,
        ).grid(row=3, column=0, pady=(4, 14), sticky="ew")
        ttk.Button(login_tab, text="Login", command=self.login).grid(
            row=4, column=0, sticky="ew"
        )

        self.register_username = tk.StringVar()
        self.register_password = tk.StringVar()
        self.register_confirm = tk.StringVar()
        ttk.Label(register_tab, text="Username").grid(row=0, column=0, sticky="w")
        ttk.Entry(
            register_tab,
            textvariable=self.register_username,
            width=32,
        ).grid(row=1, column=0, pady=(4, 10), sticky="ew")
        ttk.Label(register_tab, text="Password").grid(row=2, column=0, sticky="w")
        tk.Entry(
            register_tab,
            textvariable=self.register_password,
            show="*",
            width=32,
        ).grid(row=3, column=0, pady=(4, 10), sticky="ew")
        ttk.Label(register_tab, text="Confirm Password").grid(
            row=4, column=0, sticky="w"
        )
        tk.Entry(
            register_tab,
            textvariable=self.register_confirm,
            show="*",
            width=32,
        ).grid(row=5, column=0, pady=(4, 14), sticky="ew")
        ttk.Button(
            register_tab,
            text="Create Account",
            command=self.register,
        ).grid(row=6, column=0, sticky="ew")

    def register(self) -> None:
        username = self.register_username.get().strip()
        password = self.register_password.get()
        confirmation = self.register_confirm.get()

        if not username or len(password) < 8:
            messagebox.showerror(
                "Invalid Account",
                "Enter a username and a password with at least 8 characters.",
            )
            return
        if password != confirmation:
            messagebox.showerror("Invalid Account", "Passwords do not match.")
            return
        if not self.database.register_user(username, password):
            messagebox.showerror("Invalid Account", "That username already exists.")
            return

        messagebox.showinfo("Account Created", "You can now use the Login tab.")
        self.register_username.set("")
        self.register_password.set("")
        self.register_confirm.set("")

    def login(self) -> None:
        username = self.login_username.get().strip()
        password = self.login_password.get()
        if self.database.authenticate_user(username, password):
            self.show_dashboard(username)
        else:
            messagebox.showerror("Login Failed", "Incorrect username or password.")

    def show_dashboard(self, username: str) -> None:
        self.clear_window()
        header = ttk.Frame(self.root, padding=12)
        header.pack(fill="x")
        ttk.Label(
            header,
            text=f"Signed in as {username}",
            font=("Segoe UI", 12, "bold"),
        ).pack(side="left")
        ttk.Button(
            header,
            text="Log Out",
            command=self.show_authentication,
        ).pack(side="right")

        notebook = ttk.Notebook(self.root)
        notebook.pack(padx=12, pady=(0, 12), fill="both", expand=True)

        inventory_tab = ttk.Frame(notebook, padding=12)
        tasks_tab = ttk.Frame(notebook, padding=12)
        details_tab = ttk.Frame(notebook, padding=12)
        notebook.add(inventory_tab, text="Inventory")
        notebook.add(tasks_tab, text="Tasks")
        notebook.add(details_tab, text="Record Details")

        self.build_inventory_tab(inventory_tab)
        self.build_tasks_tab(tasks_tab)
        self.details_list = tk.Listbox(details_tab, font=("Consolas", 10))
        self.details_list.pack(fill="both", expand=True)
        self.refresh_data()

    def build_inventory_tab(self, parent: ttk.Frame) -> None:
        form = ttk.LabelFrame(parent, text="Add Inventory Item", padding=10)
        form.pack(fill="x", pady=(0, 12))

        self.item_name = tk.StringVar()
        self.item_quantity = tk.StringVar()
        self.item_location = tk.StringVar()
        for column in range(4):
            form.columnconfigure(column, weight=1)

        ttk.Label(form, text="Name").grid(row=0, column=0, sticky="w")
        ttk.Label(form, text="Quantity").grid(row=0, column=1, sticky="w")
        ttk.Label(form, text="Location").grid(row=0, column=2, sticky="w")
        ttk.Entry(form, textvariable=self.item_name).grid(
            row=1, column=0, padx=(0, 8), sticky="ew"
        )
        ttk.Entry(form, textvariable=self.item_quantity).grid(
            row=1, column=1, padx=(0, 8), sticky="ew"
        )
        ttk.Entry(form, textvariable=self.item_location).grid(
            row=1, column=2, padx=(0, 8), sticky="ew"
        )
        ttk.Button(form, text="Add Item", command=self.add_item).grid(
            row=1, column=3, sticky="ew"
        )

        columns = ("id", "name", "quantity", "location")
        self.inventory_tree = ttk.Treeview(
            parent,
            columns=columns,
            show="headings",
            height=13,
        )
        for column, heading in zip(
            columns,
            ("ID", "Name", "Quantity", "Location"),
        ):
            self.inventory_tree.heading(column, text=heading)
        self.inventory_tree.column("id", width=60, anchor="center")
        self.inventory_tree.column("quantity", width=100, anchor="center")
        self.inventory_tree.pack(fill="both", expand=True)

    def build_tasks_tab(self, parent: ttk.Frame) -> None:
        form = ttk.LabelFrame(parent, text="Add Inventory Task", padding=10)
        form.pack(fill="x", pady=(0, 12))
        form.columnconfigure(0, weight=1)
        form.columnconfigure(1, weight=2)

        self.task_item = tk.StringVar()
        self.task_description = tk.StringVar()
        ttk.Label(form, text="Inventory Item").grid(row=0, column=0, sticky="w")
        ttk.Label(form, text="Task Description").grid(row=0, column=1, sticky="w")
        self.task_item_combo = ttk.Combobox(
            form,
            textvariable=self.task_item,
            state="readonly",
        )
        self.task_item_combo.grid(row=1, column=0, padx=(0, 8), sticky="ew")
        ttk.Entry(form, textvariable=self.task_description).grid(
            row=1, column=1, padx=(0, 8), sticky="ew"
        )
        ttk.Button(form, text="Add Task", command=self.add_task).grid(
            row=1, column=2, sticky="ew"
        )

        columns = ("id", "item", "description", "status")
        self.task_tree = ttk.Treeview(
            parent,
            columns=columns,
            show="headings",
            height=11,
            selectmode="browse",
        )
        for column, heading in zip(
            columns,
            ("ID", "Item", "Description", "Status"),
        ):
            self.task_tree.heading(column, text=heading)
        self.task_tree.column("id", width=60, anchor="center")
        self.task_tree.column("status", width=100, anchor="center")
        self.task_tree.pack(fill="both", expand=True)
        ttk.Button(
            parent,
            text="Mark Selected Done",
            command=self.complete_selected_task,
        ).pack(pady=(10, 0), anchor="e")

    def add_item(self) -> None:
        name = self.item_name.get().strip()
        location = self.item_location.get().strip()
        try:
            quantity = int(self.item_quantity.get())
        except ValueError:
            quantity = -1

        if not name or not location or quantity < 0:
            messagebox.showerror(
                "Invalid Item",
                "Enter a name, a non-negative whole quantity, and a location.",
            )
            return

        self.database.add_inventory_item(name, quantity, location)
        self.item_name.set("")
        self.item_quantity.set("")
        self.item_location.set("")
        self.refresh_data()

    def add_task(self) -> None:
        description = self.task_description.get().strip()
        item_id = self.item_lookup.get(self.task_item.get())
        if item_id is None or not description:
            messagebox.showerror(
                "Invalid Task",
                "Select an inventory item and enter a task description.",
            )
            return

        self.database.add_task(item_id, description)
        self.task_description.set("")
        self.refresh_data()

    def complete_selected_task(self) -> None:
        selected = self.task_tree.selection()
        if not selected:
            messagebox.showwarning("No Task", "Select a task first.")
            return
        task_id = int(self.task_tree.item(selected[0], "values")[0])
        self.database.complete_task(task_id)
        self.refresh_data()

    def refresh_data(self) -> None:
        items = self.database.list_inventory_items()
        tasks = self.database.list_tasks()

        for row_id in self.inventory_tree.get_children():
            self.inventory_tree.delete(row_id)
        for item in items:
            self.inventory_tree.insert(
                "",
                "end",
                values=(
                    item.record_id,
                    item.title,
                    item.quantity,
                    item.location,
                ),
            )

        self.item_lookup = {
            f"{item.record_id}: {item.title}": item.record_id for item in items
        }
        item_labels = list(self.item_lookup)
        self.task_item_combo["values"] = item_labels
        if item_labels and self.task_item.get() not in self.item_lookup:
            self.task_item.set(item_labels[0])
        if not item_labels:
            self.task_item.set("")

        for row_id in self.task_tree.get_children():
            self.task_tree.delete(row_id)
        for task in tasks:
            self.task_tree.insert(
                "",
                "end",
                values=(
                    task.record_id,
                    task.item_name,
                    task.title,
                    task.status,
                ),
            )

        self.details_list.delete(0, tk.END)
        records = [*items, *tasks]
        for record in records:
            self.details_list.insert(tk.END, record.get_details())

    def close(self) -> None:
        self.database.close()
        self.root.destroy()


if __name__ == "__main__":
    root = tk.Tk()
    InventoryApp(root)
    root.mainloop()

What should the full file contain?

This reference keeps authentication, persistent inventory, relational tasks, polymorphic details, and database cleanup inside one application lifecycle. Your file should match it before the final workflow check.

Verify the complete workflow

The final check follows one task from item selection to completion. It also proves that both subclasses can describe themselves through the same interface.

Before you run the application, predict what the task table and Record Details tab should show after you complete a task.

  • Launch the completed application by running:
python myscript.py

What does the final run test?

This run exercises the authenticated dashboard against the persistent database. The following actions test inventory creation, relational task creation, status updates, and polymorphic descriptions.

  • Enter your existing username in the Username field.
  • Enter your existing password in the Password field.
  • Click Login.
  • Select the Inventory tab.
  • Enter Barcode Scanner in the Name field.
  • Enter 4 in the Quantity field.
  • Enter Shelf A in the Location field.
  • Click Add Item.

You should see Barcode Scanner in the inventory table with quantity 4 and location Shelf A.

  • Select the Tasks tab.
  • Choose the inventory item label ending in Barcode Scanner from the Inventory Item selector.
  • Enter Calibrate scanner in the Task Description field.
  • Click Add Task.

You should see a new task row linking Calibrate scanner to Barcode Scanner. Its status should be Open.

  • Select the Calibrate scanner row in the task table.
  • Click Mark Selected Done.

You should see the selected task's status change from Open to Done.

  • Select the Record Details tab.

You should see Inventory: Barcode Scanner | Quantity: 4 | Location: Shelf A and Task: Calibrate scanner | Item: Barcode Scanner | Status: Done in the list.

Task workflow not updating?

  • Confirm you selected an inventory item before clicking Add Task.
  • Confirm you selected the task row before clicking Mark Selected Done.
  • Check that both action methods finish with self.refresh_data().

Ask for help: Help me debug why my related task, Done status, or polymorphic record details are not updating.

That completes the main build. Your authenticated desktop app now turns persistent inventory into related and trackable work.

Secret mission

Highlight Low-Stock Items

Your inventory table shows quantities, but urgent shortages can still blend into the list. Add conditional row highlighting so quantities of 5 or lower receive a pale warning background while larger quantities keep the normal style.

Clean Up Your Resources

Clean Up Your Resources

Your project runs entirely on your Windows computer, so there are no ongoing cloud or service charges. Choose whether to keep the application ready, pause your work, or remove its stored data.

Resources you used:

  • Source files named models.py, database.py, and myscript.py in the open company-inventory-manager folder.
  • Local SQLite database file named company_inventory.db containing your accounts, inventory items, and tasks.

Keep everything running

No action is needed. Choose this option if you want to keep using Company Inventory Manager with its current records.

  • Keep company_inventory.db in the company-inventory-manager folder to retain your accounts, inventory items, and tasks.
  • Keep models.py, database.py, and myscript.py for future changes.
  • Return to the integrated terminal when you want to run the application again.

Pause - I'll come back to this later

Closing the Tkinter window stops the local application. No process or paid service continues running.

  • Close the Company Inventory Manager window.
  • Check that the integrated terminal has returned to its command prompt.
  • Leave the files in company-inventory-manager unchanged for your next session.

Delete - I don't want to use this again

Remove the stored application data when you want a clean slate. Deleting company_inventory.db permanently removes its accounts, inventory items, and tasks.

Your source code stays in the project folder for future reference.

  • Close the Company Inventory Manager window.
  • Switch back to Visual Studio Code from earlier.
  • Find company_inventory.db in the file sidebar.
  • Right-click company_inventory.db.
  • Choose the deletion option from the context menu.
  • Confirm the deletion if Windows asks for confirmation.
  • Check the file sidebar after deletion.

You should no longer see company_inventory.db. The files models.py, database.py, and myscript.py remain in company-inventory-manager.

Nice Work!

Nice Work!

That is a substantial build. You completed Company Inventory Manager, an authenticated Tkinter desktop application with related company records stored in SQLite.

You've learned how to:

  • Built an authenticated desktop workflow for inventory management. Added linked task creation with completion tracking.
  • Created an abstract base class for shared record behavior. Kept record state private through encapsulation. Reused behavior through inheritance. Displayed subtype-specific output through polymorphism using overridden get_details() methods.
  • Persisted the application's full data model in related SQLite tables. Enforced item-task links through foreign keys. Bound user values with parameterized SQL. Protected account access with salted password hashes.
  • Secret Mission: Added low-stock highlighting for inventory quantities of 5 or lower. Kept the relational model untouched.

Ready to quiz yourself?