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. 터치스크린 GUI, 바코드 하드웨어 연동, 영속적 상태 관리를 포함한 풀스택 이벤트 기반 재고 관리 시스템입니다.
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. 이 캡스톤 프로젝트는 수기 체크리스트를 자동화된 디지털 시스템으로 대체해 창고 운영을 현대화하는 것을 목표로 했습니다. 산업 환경에 적합한 고대비 UI와 견고한 재고 추적 백엔드 로직을 결합한 풀스택 구조로 설계했습니다.
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. Tkinter로 만든 키오스크 모드 GUI는 이벤트 리스너로 바코드 스캐너 입력을 비동기로 처리합니다. 백엔드는 JSON을 원자적으로 기록해(atomic write) 정전 등 예외 상황에서도 데이터 무결성을 유지합니다.
System Architecture시스템 아키텍처
Event-Driven GUI이벤트 기반 GUI
Implemented a responsive Tkinter interface that separates the UI thread from logic processing, ensuring the scan input never freezes the display.UI 스레드와 로직 처리를 분리해 스캔 입력이 화면을 멈추지 않도록 반응형 Tkinter UI를 구현했습니다.
Hardware Integration하드웨어 연동
Integrated USB Barcode Scanners as HID devices, capturing keystroke streams and parsing them into valid waybill or SKU codes.USB 바코드 스캐너를 HID 장치로 통합해 키 입력 스트림을 수집하고, 이를 waybill/SKU 코드로 파싱했습니다.
Persistent State영속 상태
Designed a custom JSON-based flat-file database handler to manage `current_stock` and `sales_history` with automated serialization.`current_stock`, `sales_history`를 자동 직렬화로 관리하는 JSON 기반 플랫파일 DB 핸들러를 설계했습니다.
Source Code소스 코드
Frontend: Warehouse GUI (`warehouse_gui.py`)프론트엔드: 창고 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`)백엔드: 재고/판매 관리 (`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.시스템 설계/하드웨어 사양/사용자 매뉴얼의 자세한 내용은 아래 문서를 참고하세요.