PY

Tkinter Shop Manager Code

Full working Python + SQLite code for Nimal Grocery Shop billing and inventory.

Interface

Tkinter + Treeview

Database

SQLite tables included

Currency

Sri Lankan Rupees

What this code includes

Beginner-friendly inventory CRUD, formatted receipt, auto bill number, stock deduction, validation, and bill printing.

✓ Add, update, delete products✓ Product list with Treeview✓ BILL001 auto-increment numbers✓ LKR 1,250.00 currency format✓ Receipt with shop name, date, items, total✓ Print Bill button for receipt printing✓ SQLite products, bills, bill_items, sales tables✓ Low stock warning✓ Clear bill and simple error handling

Full working Python code

Save as small_shop_inventory_billing.py and run with Python 3.

import os
import sqlite3
import tempfile
from datetime import datetime
import tkinter as tk
from tkinter import ttk, messagebox

# =====================================================
# Small Shop Inventory and Billing Management System
# Python + Tkinter + SQLite
# Beginner-friendly student project
# =====================================================

DB_NAME = "shop_inventory.db"
SHOP_NAME = "Nimal Grocery Shop"
SHOP_ADDRESS = "Main Street, Colombo"
SHOP_CONTACT = "Contact: 077 123 4567"
LOW_STOCK_LIMIT = 5


def format_lkr(amount):
    """Format money values as Sri Lankan Rupees."""
    return f"LKR {amount:,.2f}"


class InventoryBillingApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Small Shop Inventory and Billing Management System")
        self.root.geometry("1150x700")
        self.root.configure(bg="#f4f6f8")

        # Variables used by the program
        self.selected_product_id = None
        self.bill_items = []
        self.current_receipt = ""

        # Start application
        self.create_database()
        self.create_widgets()
        self.load_products()
        self.update_bill_number_label()

    # =====================================================
    # DATABASE SETUP
    # =====================================================
    def create_database(self):
        """Create all required database tables if they do not already exist."""
        conn = sqlite3.connect(DB_NAME)
        cursor = conn.cursor()

        cursor.execute("""
            CREATE TABLE IF NOT EXISTS products (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL UNIQUE,
                price REAL NOT NULL,
                quantity INTEGER NOT NULL
            )
        """)

        cursor.execute("""
            CREATE TABLE IF NOT EXISTS bills (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                bill_number TEXT NOT NULL UNIQUE,
                total_amount REAL NOT NULL,
                date TEXT NOT NULL
            )
        """)

        cursor.execute("""
            CREATE TABLE IF NOT EXISTS bill_items (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                bill_id INTEGER NOT NULL,
                product_id INTEGER NOT NULL,
                quantity INTEGER NOT NULL,
                subtotal REAL NOT NULL,
                FOREIGN KEY (bill_id) REFERENCES bills(id),
                FOREIGN KEY (product_id) REFERENCES products(id)
            )
        """)

        # Simple sales table requested in the assignment.
        # One row is saved for each product line sold.
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS sales (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                bill_number TEXT NOT NULL,
                product_id INTEGER NOT NULL,
                product_name TEXT NOT NULL,
                quantity INTEGER NOT NULL,
                unit_price REAL NOT NULL,
                subtotal REAL NOT NULL,
                date TEXT NOT NULL,
                FOREIGN KEY (product_id) REFERENCES products(id)
            )
        """)

        conn.commit()
        conn.close()

    # =====================================================
    # GUI DESIGN
    # =====================================================
    def create_widgets(self):
        title = tk.Label(
            self.root,
            text="Small Shop Inventory and Billing Management System",
            font=("Arial", 18, "bold"),
            bg="#2c3e50",
            fg="white",
            pady=12
        )
        title.pack(fill=tk.X)

        main_frame = tk.Frame(self.root, bg="#f4f6f8")
        main_frame.pack(fill=tk.BOTH, expand=True, padx=12, pady=12)

        left_frame = tk.Frame(main_frame, bg="#f4f6f8")
        left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 8))

        right_frame = tk.Frame(main_frame, bg="#f4f6f8")
        right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(8, 0))

        # Product management section
        form_frame = tk.LabelFrame(left_frame, text="Product Management", font=("Arial", 12, "bold"), bg="#f4f6f8", padx=10, pady=10)
        form_frame.pack(fill=tk.X)

        tk.Label(form_frame, text="Product Name", bg="#f4f6f8").grid(row=0, column=0, sticky="w", pady=5)
        self.name_entry = tk.Entry(form_frame, width=30)
        self.name_entry.grid(row=0, column=1, pady=5, padx=5)

        tk.Label(form_frame, text="Price", bg="#f4f6f8").grid(row=1, column=0, sticky="w", pady=5)
        self.price_entry = tk.Entry(form_frame, width=30)
        self.price_entry.grid(row=1, column=1, pady=5, padx=5)

        tk.Label(form_frame, text="Quantity", bg="#f4f6f8").grid(row=2, column=0, sticky="w", pady=5)
        self.quantity_entry = tk.Entry(form_frame, width=30)
        self.quantity_entry.grid(row=2, column=1, pady=5, padx=5)

        button_frame = tk.Frame(form_frame, bg="#f4f6f8")
        button_frame.grid(row=3, column=0, columnspan=2, pady=10)

        tk.Button(button_frame, text="Add Product", width=14, bg="#27ae60", fg="white", command=self.add_product).grid(row=0, column=0, padx=4)
        tk.Button(button_frame, text="Update Product", width=14, bg="#2980b9", fg="white", command=self.update_product).grid(row=0, column=1, padx=4)
        tk.Button(button_frame, text="Delete Product", width=14, bg="#c0392b", fg="white", command=self.delete_product).grid(row=0, column=2, padx=4)
        tk.Button(button_frame, text="Clear Fields", width=14, command=self.clear_product_fields).grid(row=0, column=3, padx=4)

        # Product list table section
        table_frame = tk.LabelFrame(left_frame, text="Product List", font=("Arial", 12, "bold"), bg="#f4f6f8", padx=10, pady=10)
        table_frame.pack(fill=tk.BOTH, expand=True, pady=10)

        columns = ("id", "name", "price", "quantity", "status")
        self.product_tree = ttk.Treeview(table_frame, columns=columns, show="headings", height=14)
        self.product_tree.heading("id", text="ID")
        self.product_tree.heading("name", text="Product Name")
        self.product_tree.heading("price", text="Price")
        self.product_tree.heading("quantity", text="Quantity")
        self.product_tree.heading("status", text="Stock Status")

        self.product_tree.column("id", width=50, anchor="center")
        self.product_tree.column("name", width=180)
        self.product_tree.column("price", width=110, anchor="e")
        self.product_tree.column("quantity", width=90, anchor="center")
        self.product_tree.column("status", width=120, anchor="center")
        self.product_tree.pack(fill=tk.BOTH, expand=True)
        self.product_tree.bind("<<TreeviewSelect>>", self.on_product_select)

        # Billing area section
        billing_frame = tk.LabelFrame(right_frame, text="Billing Area", font=("Arial", 12, "bold"), bg="#f4f6f8", padx=10, pady=10)
        billing_frame.pack(fill=tk.X)

        self.bill_number_label = tk.Label(billing_frame, text="Bill No: BILL001", font=("Arial", 11, "bold"), bg="#f4f6f8")
        self.bill_number_label.grid(row=0, column=0, columnspan=2, sticky="w", pady=5)

        tk.Label(billing_frame, text="Selected Product", bg="#f4f6f8").grid(row=1, column=0, sticky="w", pady=5)
        self.selected_product_label = tk.Label(billing_frame, text="None", bg="#f4f6f8", font=("Arial", 10, "bold"))
        self.selected_product_label.grid(row=1, column=1, sticky="w", pady=5)

        tk.Label(billing_frame, text="Quantity", bg="#f4f6f8").grid(row=2, column=0, sticky="w", pady=5)
        self.bill_quantity_entry = tk.Entry(billing_frame, width=20)
        self.bill_quantity_entry.grid(row=2, column=1, sticky="w", pady=5)

        tk.Button(billing_frame, text="Add Item to Bill", bg="#8e44ad", fg="white", width=18, command=self.add_item_to_bill).grid(row=3, column=0, pady=10)
        tk.Button(billing_frame, text="Clear Bill", width=18, command=self.clear_bill).grid(row=3, column=1, pady=10, sticky="w")

        # Bill display / receipt section
        receipt_frame = tk.LabelFrame(right_frame, text="Bill Display", font=("Arial", 12, "bold"), bg="#f4f6f8", padx=10, pady=10)
        receipt_frame.pack(fill=tk.BOTH, expand=True, pady=10)

        self.receipt_text = tk.Text(receipt_frame, height=22, width=55, font=("Courier New", 10))
        self.receipt_text.pack(fill=tk.BOTH, expand=True)

        tk.Button(right_frame, text="Complete Transaction", font=("Arial", 12, "bold"), bg="#16a085", fg="white", command=self.complete_transaction).pack(fill=tk.X, pady=5)
        tk.Button(right_frame, text="Print Bill", font=("Arial", 12, "bold"), bg="#34495e", fg="white", command=self.print_bill).pack(fill=tk.X, pady=5)

        self.total_label = tk.Label(right_frame, text="Total Amount: LKR 0.00", font=("Arial", 14, "bold"), bg="#f4f6f8", fg="#2c3e50")
        self.total_label.pack(anchor="e", pady=5)

        self.refresh_receipt()

    # =====================================================
    # PRODUCT MANAGEMENT FUNCTIONS
    # =====================================================
    def add_product(self):
        name = self.name_entry.get().strip()
        price_text = self.price_entry.get().strip()
        quantity_text = self.quantity_entry.get().strip()

        if not name or not price_text or not quantity_text:
            messagebox.showerror("Input Error", "Please fill all product fields.")
            return

        try:
            price = float(price_text)
            quantity = int(quantity_text)
            if price <= 0 or quantity < 0:
                raise ValueError
        except ValueError:
            messagebox.showerror("Input Error", "Price must be greater than 0 and quantity must be 0 or more.")
            return

        try:
            conn = sqlite3.connect(DB_NAME)
            cursor = conn.cursor()
            cursor.execute("INSERT INTO products (name, price, quantity) VALUES (?, ?, ?)", (name, price, quantity))
            conn.commit()
            conn.close()
            messagebox.showinfo("Success", "Product added successfully.")
            self.clear_product_fields()
            self.load_products()
        except sqlite3.IntegrityError:
            messagebox.showerror("Database Error", "Product name already exists.")

    def update_product(self):
        if self.selected_product_id is None:
            messagebox.showerror("Selection Error", "Please select a product to update.")
            return

        name = self.name_entry.get().strip()
        price_text = self.price_entry.get().strip()
        quantity_text = self.quantity_entry.get().strip()

        if not name or not price_text or not quantity_text:
            messagebox.showerror("Input Error", "Please fill all product fields.")
            return

        try:
            price = float(price_text)
            quantity = int(quantity_text)
            if price <= 0 or quantity < 0:
                raise ValueError
        except ValueError:
            messagebox.showerror("Input Error", "Invalid price or quantity.")
            return

        try:
            conn = sqlite3.connect(DB_NAME)
            cursor = conn.cursor()
            cursor.execute("UPDATE products SET name=?, price=?, quantity=? WHERE id=?", (name, price, quantity, self.selected_product_id))
            conn.commit()
            conn.close()
            messagebox.showinfo("Success", "Product updated successfully.")
            self.clear_product_fields()
            self.load_products()
        except sqlite3.IntegrityError:
            messagebox.showerror("Database Error", "Another product already uses this name.")

    def delete_product(self):
        if self.selected_product_id is None:
            messagebox.showerror("Selection Error", "Please select a product to delete.")
            return

        if not messagebox.askyesno("Confirm Delete", "Are you sure you want to delete this product?"):
            return

        conn = sqlite3.connect(DB_NAME)
        cursor = conn.cursor()
        cursor.execute("DELETE FROM products WHERE id=?", (self.selected_product_id,))
        conn.commit()
        conn.close()

        messagebox.showinfo("Deleted", "Product deleted successfully.")
        self.clear_product_fields()
        self.load_products()

    def load_products(self):
        """Load all products into the Treeview table."""
        for row in self.product_tree.get_children():
            self.product_tree.delete(row)

        conn = sqlite3.connect(DB_NAME)
        cursor = conn.cursor()
        cursor.execute("SELECT id, name, price, quantity FROM products ORDER BY id DESC")
        products = cursor.fetchall()
        conn.close()

        for product in products:
            product_id, name, price, quantity = product
            if quantity == 0:
                status = "Out of Stock"
            elif quantity <= LOW_STOCK_LIMIT:
                status = "Low Stock"
            else:
                status = "Available"
            self.product_tree.insert("", tk.END, values=(product_id, name, format_lkr(price), quantity, status))

    def on_product_select(self, event):
        """Fill product fields when a product row is selected."""
        selected = self.product_tree.focus()
        if not selected:
            return

        values = self.product_tree.item(selected, "values")
        self.selected_product_id = int(values[0])
        self.name_entry.delete(0, tk.END)
        self.name_entry.insert(0, values[1])
        self.price_entry.delete(0, tk.END)
        self.price_entry.insert(0, values[2].replace("LKR", "").replace(",", "").strip())
        self.quantity_entry.delete(0, tk.END)
        self.quantity_entry.insert(0, values[3])
        self.selected_product_label.config(text=values[1])

    def clear_product_fields(self):
        self.selected_product_id = None
        self.name_entry.delete(0, tk.END)
        self.price_entry.delete(0, tk.END)
        self.quantity_entry.delete(0, tk.END)
        self.selected_product_label.config(text="None")

    # =====================================================
    # BILLING FUNCTIONS
    # =====================================================
    def get_next_bill_number(self):
        """Generate BILL001, BILL002, etc. using the highest existing bill id."""
        conn = sqlite3.connect(DB_NAME)
        cursor = conn.cursor()
        cursor.execute("SELECT MAX(id) FROM bills")
        last_id = cursor.fetchone()[0]
        conn.close()
        next_id = 1 if last_id is None else last_id + 1
        return f"BILL{next_id:03d}"

    def update_bill_number_label(self):
        self.bill_number_label.config(text=f"Bill No: {self.get_next_bill_number()}")

    def add_item_to_bill(self):
        """Add the selected product and quantity to the current bill list."""
        if self.selected_product_id is None:
            messagebox.showerror("Selection Error", "Please select a product from the product list.")
            return

        try:
            bill_quantity = int(self.bill_quantity_entry.get().strip())
            if bill_quantity <= 0:
                raise ValueError
        except ValueError:
            messagebox.showerror("Input Error", "Please enter a valid bill quantity.")
            return

        conn = sqlite3.connect(DB_NAME)
        cursor = conn.cursor()
        cursor.execute("SELECT name, price, quantity FROM products WHERE id=?", (self.selected_product_id,))
        product = cursor.fetchone()
        conn.close()

        if product is None:
            messagebox.showerror("Error", "Selected product not found.")
            return

        name, price, stock_quantity = product
        already_in_bill = sum(item["quantity"] for item in self.bill_items if item["product_id"] == self.selected_product_id)

        if bill_quantity + already_in_bill > stock_quantity:
            messagebox.showwarning("Stock Warning", f"Only {stock_quantity} unit(s) available in stock.")
            return

        item_total = price * bill_quantity
        self.bill_items.append({
            "product_id": self.selected_product_id,
            "name": name,
            "quantity": bill_quantity,
            "price": price,
            "total": item_total
        })

        if stock_quantity <= LOW_STOCK_LIMIT:
            messagebox.showwarning("Low Stock", f"Low stock warning for {name}. Only {stock_quantity} unit(s) left.")

        self.bill_quantity_entry.delete(0, tk.END)
        self.refresh_receipt()

    def refresh_receipt(self):
        """Display the current bill receipt in the bill display area."""
        self.receipt_text.delete("1.0", tk.END)
        bill_number = self.get_next_bill_number()
        now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        total_amount = sum(item["total"] for item in self.bill_items)

        receipt = ""
        receipt += f"{SHOP_NAME:^54}
"
        receipt += f"{SHOP_ADDRESS:^54}
"
        receipt += f"{SHOP_CONTACT:^54}
"
        receipt += "=" * 54 + "
"
        receipt += f"Date & Time : {now}
"
        receipt += f"Bill Number : {bill_number}
"
        receipt += "-" * 54 + "
"
        receipt += f"{'Product':<20}{'Qty':>5}{'Price':>13}{'Total':>14}
"
        receipt += "-" * 54 + "
"

        for item in self.bill_items:
            receipt += f"{item['name'][:20]:<20}{item['quantity']:>5}{item['price']:>13,.2f}{item['total']:>14,.2f}
"

        receipt += "-" * 54 + "
"
        receipt += f"{'Grand Total:':<35}{format_lkr(total_amount):>19}
"
        receipt += "=" * 54 + "
"
        receipt += f"{'Thank You! Visit Again!':^54}
"

        self.current_receipt = receipt
        self.receipt_text.insert(tk.END, receipt)
        self.total_label.config(text=f"Total Amount: {format_lkr(total_amount)}")

    def print_bill(self):
        """Print the currently displayed bill receipt."""
        if not self.bill_items:
            messagebox.showerror("Print Error", "Please add at least one item before printing the bill.")
            return

        try:
            # Save receipt to a temporary text file and send it to the default printer.
            temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".txt", mode="w", encoding="utf-8")
            temp_file.write(self.current_receipt)
            temp_file.close()

            if os.name == "nt":
                os.startfile(temp_file.name, "print")
                messagebox.showinfo("Print", "Bill sent to printer.")
            else:
                messagebox.showinfo("Print File Created", f"Bill saved for printing:
{temp_file.name}")
        except Exception as error:
            messagebox.showerror("Print Error", f"Could not print the bill: {error}")

    def complete_transaction(self):
        """Save bill, save bill items/sales, reduce stock, and clear current bill."""
        if not self.bill_items:
            messagebox.showerror("Bill Error", "No items added to the bill.")
            return

        bill_number = self.get_next_bill_number()
        bill_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        total_amount = sum(item["total"] for item in self.bill_items)

        conn = sqlite3.connect(DB_NAME)
        cursor = conn.cursor()

        try:
            # Save main bill record
            cursor.execute("INSERT INTO bills (bill_number, total_amount, date) VALUES (?, ?, ?)", (bill_number, total_amount, bill_date))
            bill_id = cursor.lastrowid

            # Save each bill item, save a sales row, and reduce stock
            for item in self.bill_items:
                cursor.execute(
                    "INSERT INTO bill_items (bill_id, product_id, quantity, subtotal) VALUES (?, ?, ?, ?)",
                    (bill_id, item["product_id"], item["quantity"], item["total"])
                )
                cursor.execute(
                    "INSERT INTO sales (bill_number, product_id, product_name, quantity, unit_price, subtotal, date) VALUES (?, ?, ?, ?, ?, ?, ?)",
                    (bill_number, item["product_id"], item["name"], item["quantity"], item["price"], item["total"], bill_date)
                )
                cursor.execute("UPDATE products SET quantity = quantity - ? WHERE id=?", (item["quantity"], item["product_id"]))

            conn.commit()
            messagebox.showinfo("Success", f"Transaction completed.
Bill Number: {bill_number}
Total: {format_lkr(total_amount)}")
            self.clear_bill()
            self.clear_product_fields()
            self.load_products()
            self.update_bill_number_label()
        except sqlite3.Error as error:
            conn.rollback()
            messagebox.showerror("Database Error", f"Transaction failed: {error}")
        finally:
            conn.close()

    def clear_bill(self):
        """Clear all items from the current bill."""
        self.bill_items = []
        self.bill_quantity_entry.delete(0, tk.END)
        self.refresh_receipt()


# Run the application
if __name__ == "__main__":
    root = tk.Tk()
    app = InventoryBillingApp(root)
    root.mainloop()
Built with GenMB
Built with GenMB