Automated Warehouse Inventory System
A full-stack, event-driven inventory management system featuring a touch-screen GUI, barcode hardware integration, and persistent database state management.
Project Overview
This capstone project was designed to modernize warehouse operations by replacing manual checklists with an automated digital system. I architected a full-stack solution that integrates high-contrast UI for industrial environments with robust backend logic for inventory tracking.
The system features a Kiosk Mode GUI built with Tkinter, utilizing event listeners to handle barcode scanner input asynchronously. The backend maintains ACID-like properties using atomic JSON write operations to ensure stock data integrity even during power failures.
System Architecture
Event-Driven GUI
Implemented a responsive Tkinter interface that separates the UI thread from
logic processing, ensuring the scan input never freezes the display.
Hardware Integration
Integrated USB Barcode Scanners as HID devices, capturing keystroke streams and parsing them into valid waybill or SKU codes.
Persistent State
Designed a custom JSON-based flat-file database handler to manage `current_stock` and `sales_history` with automated serialization.
Source Code
Frontend: Warehouse GUI (`warehouse_gui.py`)
#!/usr/bin/python3
#Fianl version: 12/08 22:10
#I removed the audio feedback module because the Raspberry Pi 5’s internal
#audio hardware drivers proved inconsistent across different OS updates.
#To ensure the system remains robust and crash-proof during the demo,
# I decided to rely exclusively on the high-contrast visual GUI
#(Red/Green alerts), which provides 100% reliability without external hardware
# dependencies. On top of that, relying to a beeper in a warehouse where hundreds
# of orders are processed everyday may not be a smart decision.
#
#
#Emojis imported for intuitive UI/UX, copy and pasted from Noto Color Emoji. That
#makes these emojis not-os dependent.
"""
Warehouse Scanner - GUI Version with Tkinter
"""
"""
Warehouse Scanner - GUI Version with Tkinter
"""
import tkinter as tk
from tkinter import ttk, messagebox
from barcode_info import lookup_barcode
from waybill_database import get_waybill_order, is_waybill
from inventory_stock import check_stock_availability, deduct_stock, record_sale
class WarehouseGUI:
def __init__(self, root):
self.root = root
self.root.title("Warehouse System")
#KIOSK MODE: Fullscreen
self.root.attributes('-fullscreen', True)
self.root.bind("", lambda e: self.root.destroy())
self.current_order = None
self.required_items = {}
self.scanned_items = {}
self.setup_ui()
self.barcode_entry.focus_set()
def setup_ui(self):
#Title
tk.Label(self.root, text="📦 WAREHOUSE SYSTEM", font=('Arial', 24, 'bold'), bg='#2c3e50', fg='white', pady=20).pack(fill=tk.X)
#Input Area
input_frame = tk.Frame(self.root, pady=20)
input_frame.pack(fill=tk.X)
tk.Label(input_frame, text="SCAN BARCODE:", font=('Arial', 14)).pack(side=tk.LEFT, padx=20)
self.barcode_entry = tk.Entry(input_frame, font=('Arial', 14), width=30)
self.barcode_entry.pack(side=tk.LEFT, padx=10)
self.barcode_entry.bind('', self.on_scan)
self.status_label = tk.Label(input_frame, text="READY - SCAN WAYBILL", font=('Arial', 14), fg='green')
self.status_label.pack(side=tk.LEFT, padx=20)
#Columns
cols = tk.Frame(self.root, padx=20, pady=10)
cols.pack(fill=tk.BOTH, expand=True)
#Left: Required Items
left = tk.LabelFrame(cols, text="REQUIRED", font=('Arial', 12, 'bold'))
left.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5)
self.tree_req = ttk.Treeview(left, columns=('Item','Qty'), show='headings', height=10)
self.tree_req.heading('Item', text='Product'); self.tree_req.column('Item', width=300)
self.tree_req.heading('Qty', text='Qty'); self.tree_req.column('Qty', width=50)
self.tree_req.pack(fill=tk.BOTH, expand=True)
#Right: Scanned Items
right = tk.LabelFrame(cols, text="SCANNED", font=('Arial', 12, 'bold'))
right.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5)
self.tree_scan = ttk.Treeview(right, columns=('Item','Status'), show='headings', height=10)
self.tree_scan.heading('Item', text='Product'); self.tree_scan.column('Item', width=300)
self.tree_scan.heading('Status', text='Status'); self.tree_scan.column('Status', width=100)
self.tree_scan.pack(fill=tk.BOTH, expand=True)
#Bottom Buttons
btn_frame = tk.Frame(self.root, pady=20)
btn_frame.pack(fill=tk.X)
tk.Button(btn_frame, text="EXIT (Esc)", command=self.root.destroy, bg='#e74c3c', fg='white', font=('Arial', 12)).pack(side=tk.RIGHT, padx=20)
def on_scan(self, event):
code = self.barcode_entry.get().strip()
self.barcode_entry.delete(0, tk.END)
if not code: return
if not self.current_order:
self.process_waybill(code)
else:
self.process_item(code)
def process_waybill(self, code):
if not is_waybill(code):
self.alert("INVALID WAYBILL", error=True)
return
order = get_waybill_order(code)
#Check Stock
ok, missing = check_stock_availability(order['items'])
if not ok:
messagebox.showerror("STOCK ERROR", "\n".join(missing))
return
self.current_order = order
self.required_items = order['items']
self.scanned_items = {k:0 for k in self.required_items}
self.update_ui()
self.status_label.config(text=f"ORDER: {order['customer_name']}", fg='blue')
def process_item(self, code):
product = lookup_barcode(code)
if not product:
self.alert("UNKNOWN BARCODE", error=True)
return
name = product['name']
if name not in self.required_items:
self.alert("WRONG ITEM!", error=True)
return
if self.scanned_items[name] >= self.required_items[name]:
self.alert("ALREADY SCANNED", error=True)
return
self.scanned_items[name] += 1
self.update_ui()
self.status_label.config(text=f"✓ SCANNED: {name}", fg='green')
#Check completion
if all(self.scanned_items[k] >= self.required_items[k] for k in self.required_items):
self.finish_order()
def finish_order(self):
deduct_stock(self.required_items)
record_sale(self.current_order['order_id'], self.required_items)
messagebox.showinfo("COMPLETE", "ORDER VERIFIED & INVENTORY UPDATED")
self.reset()
def update_ui(self):
for t in (self.tree_req, self.tree_scan):
for i in t.get_children(): t.delete(i)
for item, qty in self.required_items.items():
self.tree_req.insert('', tk.END, values=(item, qty))
for item, qty in self.required_items.items():
done = self.scanned_items[item]
status = "✅ DONE" if done >= qty else f"⏳ {done}/{qty}"
self.tree_scan.insert('', tk.END, values=(item, status))
def alert(self, msg, error=False):
self.status_label.config(text=msg, fg='red' if error else 'green')
self.barcode_entry.focus_set()
def reset(self):
self.current_order = None
self.update_ui()
self.status_label.config(text="READY - SCAN WAYBILL", fg='green')
self.barcode_entry.focus_set()
if __name__ == '__main__':
root = tk.Tk()
app = WarehouseGUI(root)
root.mainloop()
Backend: Inventory & Stock Management (`inventory_stock.py`)
#!/usr/bin/python3
"""
Inventory Stock Management
"""
import json
import os
from datetime import datetime
STOCK_FILE = "current_stock.json"
SALES_FILE = "sales_history.json"
LOW_STOCK_LIMIT = 40
#Starting Inventory
INITIAL_STOCK = {
"Mini Flat Gora Dark Brown": 200,
"Mini Flat Gora Black":200,
"Mini Flat Gora Tan": 200,
"Minkmore Rabbit Keyring Black": 200,
"Minkmore Rabbit Keyring Baekseolgi": 200,
"Minkmore Rabbit Keyring Powder": 200,
"Minkmore Rabbit Keyring Bunyu": 200,
"Minkmore Rabbit Keyring Yulmucha": 200,
"Minkmore Rabbit Keyring Misutgaru": 200
}
def load_stock():
"""Load stock from disk or return initial defaults"""
if os.path.exists(STOCK_FILE):
with open(STOCK_FILE,'r') as f: return json.load(f)
return INITIAL_STOCK.copy()
def save_stock(stock):
with open(STOCK_FILE,'w') as f: json.dump(stock, f, indent=2)
def check_stock_availability(items_needed):
"""Return (True/False, List of missing items)"""
current = load_stock()
missing = []
for item, qty in items_needed.items():
if current.get(item, 0) < qty:
missing.append(f"{item} (Have: {current.get(item, 0)})")
return (len(missing) == 0, missing)
def deduct_stock(items_sold):
"""Subtract sold items from inventory"""
stock = load_stock()
for item, qty in items_sold.items():
if item in stock:
#preventing negative stock
stock[item] = max(0, stock[item] - qty)
save_stock(stock)
def get_low_stock_items():
"""Return list of items below threshold"""
stock = load_stock()
return [(k, v) for k, v in stock.items() if v<=LOW_STOCK_LIMIT]
def load_sales_history():
"""Load full sales history log"""
if os.path.exists(SALES_FILE):
with open(SALES_FILE, 'r') as f: return json.load(f)
return []
def record_sale(order_id, items_sold):
"""Log sale to history file"""
history = load_sales_history()
history.append({
"timestamp": datetime.now().isoformat(),
"order_id": order_id,
"items": items_sold
})
with open(SALES_FILE, 'w') as f: json.dump(history, f, indent=2)
def get_sales_summary(days=30):
"""Aggregate sales for last N days"""
summary = {}
history = load_sales_history()
cutoff = datetime.now().timestamp() - (days * 86400)
for sale in history:
sale_ts = datetime.fromisoformat(sale["timestamp"]).timestamp()
if sale_ts >= cutoff:
for item, qty in sale["items"].items():
summary[item] = summary.get(item, 0) + qty
return summary
Database Example: State Files
// current_stock.json (Snippet)
{
"Mini Flat Gora Dark Brown": 199,
"Mini Flat Gora Black": 199,
"Mini Flat Gora Tan": 198,
// ...
}
Full Documentation
For a deep dive into the system design, hardware specifications, and user manual, refer to the extensive documentation below.