Add inventory/POS system and enhance checklist with time intervals

Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-10-28 05:42:24 +00:00
parent 9683d8b79e
commit 8372817733
6 changed files with 869 additions and 17 deletions
+8
View File
@@ -272,6 +272,14 @@ enabled = False
checklist_db = data/checklist.db
reverse_in_out = False
# Inventory and Point of Sale System
[inventory]
enabled = False
inventory_db = data/inventory.db
# Set to True to enable penny rounding (USA cash sales)
# Rounds down for cash sales, up for taxed sales
disable_penny = False
[qrz]
# QRZ Hello to new nodes with message
enabled = False
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+203 -17
View File
@@ -6,7 +6,8 @@ from modules.log import logger
from modules.settings import checklist_db, reverse_in_out, bbs_ban_list
import time
trap_list_checklist = ("checkin", "checkout", "checklist", "purgein", "purgeout")
trap_list_checklist = ("checkin", "checkout", "checklist", "purgein", "purgeout",
"checklistapprove", "checklistdeny", "checklistadd", "checklistremove")
def initialize_checklist_database():
try:
@@ -14,10 +15,25 @@ def initialize_checklist_database():
c = conn.cursor()
# Check if the checkin table exists, and create it if it doesn't
c.execute('''CREATE TABLE IF NOT EXISTS checkin
(checkin_id INTEGER PRIMARY KEY, checkin_name TEXT, checkin_date TEXT, checkin_time TEXT, location TEXT, checkin_notes TEXT)''')
(checkin_id INTEGER PRIMARY KEY, checkin_name TEXT, checkin_date TEXT,
checkin_time TEXT, location TEXT, checkin_notes TEXT,
approved INTEGER DEFAULT 1, expected_checkin_interval INTEGER DEFAULT 0)''')
# Check if the checkout table exists, and create it if it doesn't
c.execute('''CREATE TABLE IF NOT EXISTS checkout
(checkout_id INTEGER PRIMARY KEY, checkout_name TEXT, checkout_date TEXT, checkout_time TEXT, location TEXT, checkout_notes TEXT)''')
(checkout_id INTEGER PRIMARY KEY, checkout_name TEXT, checkout_date TEXT,
checkout_time TEXT, location TEXT, checkout_notes TEXT)''')
# Add new columns if they don't exist (for migration)
try:
c.execute("ALTER TABLE checkin ADD COLUMN approved INTEGER DEFAULT 1")
except sqlite3.OperationalError:
pass # Column already exists
try:
c.execute("ALTER TABLE checkin ADD COLUMN expected_checkin_interval INTEGER DEFAULT 0")
except sqlite3.OperationalError:
pass # Column already exists
conn.commit()
conn.close()
return True
@@ -110,6 +126,131 @@ def delete_checkout(checkout_id):
conn.close()
return "Checkout deleted." + str(checkout_id)
def approve_checkin(checkin_id):
"""Approve a pending check-in"""
conn = sqlite3.connect(checklist_db)
c = conn.cursor()
try:
c.execute("UPDATE checkin SET approved = 1 WHERE checkin_id = ?", (checkin_id,))
if c.rowcount == 0:
conn.close()
return f"Check-in ID {checkin_id} not found."
conn.commit()
conn.close()
return f"✅ Check-in {checkin_id} approved."
except Exception as e:
conn.close()
logger.error(f"Checklist: Error approving check-in: {e}")
return "Error approving check-in."
def deny_checkin(checkin_id):
"""Deny/delete a pending check-in"""
conn = sqlite3.connect(checklist_db)
c = conn.cursor()
try:
c.execute("DELETE FROM checkin WHERE checkin_id = ?", (checkin_id,))
if c.rowcount == 0:
conn.close()
return f"Check-in ID {checkin_id} not found."
conn.commit()
conn.close()
return f"❌ Check-in {checkin_id} denied and removed."
except Exception as e:
conn.close()
logger.error(f"Checklist: Error denying check-in: {e}")
return "Error denying check-in."
def set_checkin_interval(name, interval_minutes):
"""Set expected check-in interval for a user (for safety monitoring)"""
conn = sqlite3.connect(checklist_db)
c = conn.cursor()
try:
# Update the most recent active check-in for this user
c.execute("""
UPDATE checkin
SET expected_checkin_interval = ?
WHERE checkin_name = ?
AND checkin_id NOT IN (
SELECT checkin_id FROM checkout
WHERE checkout_name = checkin_name
AND (checkout_date > checkin_date OR (checkout_date = checkin_date AND checkout_time > checkin_time))
)
ORDER BY checkin_date DESC, checkin_time DESC
LIMIT 1
""", (interval_minutes, name))
if c.rowcount == 0:
conn.close()
return f"No active check-in found for {name}."
conn.commit()
conn.close()
return f"⏰ Check-in interval set to {interval_minutes} minutes for {name}."
except Exception as e:
conn.close()
logger.error(f"Checklist: Error setting check-in interval: {e}")
return "Error setting check-in interval."
def get_overdue_checkins():
"""Get list of users who haven't checked in within their expected interval"""
conn = sqlite3.connect(checklist_db)
c = conn.cursor()
current_time = time.time()
try:
c.execute("""
SELECT checkin_id, checkin_name, checkin_date, checkin_time, expected_checkin_interval, location
FROM checkin
WHERE expected_checkin_interval > 0
AND approved = 1
AND checkin_id NOT IN (
SELECT checkin_id FROM checkout
WHERE checkout_name = checkin_name
AND (checkout_date > checkin_date OR (checkout_date = checkin_date AND checkout_time > checkin_time))
)
""")
active_checkins = c.fetchall()
conn.close()
overdue_list = []
for checkin_id, name, date, time_str, interval, location in active_checkins:
checkin_datetime = time.mktime(time.strptime(f"{date} {time_str}", "%Y-%m-%d %H:%M:%S"))
time_since_checkin = (current_time - checkin_datetime) / 60 # in minutes
if time_since_checkin > interval:
overdue_minutes = int(time_since_checkin - interval)
overdue_list.append({
'id': checkin_id,
'name': name,
'location': location,
'overdue_minutes': overdue_minutes,
'interval': interval
})
return overdue_list
except Exception as e:
conn.close()
logger.error(f"Checklist: Error getting overdue check-ins: {e}")
return []
def format_overdue_alert():
"""Format overdue check-ins as an alert message"""
overdue = get_overdue_checkins()
if not overdue:
return None
alert = "⚠️ OVERDUE CHECK-INS:\n"
for entry in overdue:
hours = entry['overdue_minutes'] // 60
minutes = entry['overdue_minutes'] % 60
alert += f"{entry['name']}: {hours}h {minutes}m overdue"
if entry['location']:
alert += f" @ {entry['location']}"
alert += "\n"
return alert.rstrip()
def list_checkin():
# list checkins
conn = sqlite3.connect(checklist_db)
@@ -153,31 +294,76 @@ def process_checklist_command(nodeID, message, name="none", location="none"):
if str(nodeID) in bbs_ban_list:
logger.warning("System: Checklist attempt from the ban list")
return "unable to process command"
message_lower = message.lower()
parts = message.split()
try:
comment = message.split(" ", 1)[1]
comment = message.split(" ", 1)[1] if len(parts) > 1 else ""
except IndexError:
comment = ""
# handle checklist commands
if ("checkin" in message.lower() and not reverse_in_out) or ("checkout" in message.lower() and reverse_in_out):
return checkin(name, current_date, current_time, location, comment)
elif ("checkout" in message.lower() and not reverse_in_out) or ("checkin" in message.lower() and reverse_in_out):
if ("checkin" in message_lower and not reverse_in_out) or ("checkout" in message_lower and reverse_in_out):
# Check if interval is specified: checkin 60 comment
interval = 0
actual_comment = comment
if comment and parts[1].isdigit():
interval = int(parts[1])
actual_comment = " ".join(parts[2:]) if len(parts) > 2 else ""
result = checkin(name, current_date, current_time, location, actual_comment)
# Set interval if specified
if interval > 0:
set_checkin_interval(name, interval)
result += f" (monitoring every {interval}min)"
return result
elif ("checkout" in message_lower and not reverse_in_out) or ("checkin" in message_lower and reverse_in_out):
return checkout(name, current_date, current_time, location, comment)
elif "purgein" in message.lower():
elif "purgein" in message_lower:
return delete_checkin(nodeID)
elif "purgeout" in message.lower():
elif "purgeout" in message_lower:
return delete_checkout(nodeID)
elif "?" in message.lower():
elif message_lower.startswith("checklistapprove "):
try:
checkin_id = int(parts[1])
return approve_checkin(checkin_id)
except (ValueError, IndexError):
return "Usage: checklistapprove <checkin_id>"
elif message_lower.startswith("checklistdeny "):
try:
checkin_id = int(parts[1])
return deny_checkin(checkin_id)
except (ValueError, IndexError):
return "Usage: checklistdeny <checkin_id>"
elif "?" in message_lower:
if not reverse_in_out:
return ("Command: checklist followed by\n"
"checkout to check out\n"
"purgeout to delete your checkout record\n"
"Example: checkin Arrived at park")
"checkin [interval] [note] - check in (optional interval in minutes)\n"
"checkout [note] - check out\n"
"purgein - delete your checkin\n"
"purgeout - delete your checkout\n"
"checklistapprove <id> - approve checkin\n"
"checklistdeny <id> - deny checkin\n"
"Example: checkin 60 Hunting in tree stand")
else:
return ("Command: checklist followed by\n"
"checkin to check out\n"
"purgeout to delete your checkin record\n"
"Example: checkout Leaving park")
elif "checklist" in message.lower():
"checkout [interval] [note] - check out (optional interval)\n"
"checkin [note] - check in\n"
"purgeout - delete your checkout\n"
"purgein - delete your checkin\n"
"Example: checkout 60 Leaving park")
elif "checklist" in message_lower:
return list_checkin()
else:
return "Invalid command."
+649
View File
@@ -0,0 +1,649 @@
# Inventory and Point of Sale module for the bot
# K7MHI Kelly Keeton 2024
# Enhanced POS system with cart management and inventory tracking
import sqlite3
from modules.log import logger
from modules.settings import inventory_db, disable_penny, bbs_ban_list
import time
from decimal import Decimal, ROUND_HALF_UP, ROUND_DOWN
trap_list_inventory = ("item", "itemlist", "itemsell", "itemreturn", "itemadd", "itemremove",
"itemreset", "itemstats", "cart", "cartadd", "cartremove", "cartlist",
"cartbuy", "cartsell", "cartclear")
def initialize_inventory_database():
"""Initialize the inventory database with all necessary tables"""
try:
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
# Items table - stores inventory items
c.execute('''CREATE TABLE IF NOT EXISTS items
(item_id INTEGER PRIMARY KEY AUTOINCREMENT,
item_name TEXT UNIQUE NOT NULL,
item_price REAL NOT NULL,
item_quantity INTEGER NOT NULL DEFAULT 0,
location TEXT,
created_date TEXT,
updated_date TEXT)''')
# Transactions table - stores sales/purchases
c.execute('''CREATE TABLE IF NOT EXISTS transactions
(transaction_id INTEGER PRIMARY KEY AUTOINCREMENT,
transaction_type TEXT NOT NULL,
transaction_date TEXT NOT NULL,
transaction_time TEXT NOT NULL,
user_name TEXT,
total_amount REAL NOT NULL,
notes TEXT)''')
# Transaction items table - stores items in each transaction
c.execute('''CREATE TABLE IF NOT EXISTS transaction_items
(id INTEGER PRIMARY KEY AUTOINCREMENT,
transaction_id INTEGER NOT NULL,
item_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
price_at_sale REAL NOT NULL,
FOREIGN KEY (transaction_id) REFERENCES transactions(transaction_id),
FOREIGN KEY (item_id) REFERENCES items(item_id))''')
# Carts table - stores temporary shopping carts
c.execute('''CREATE TABLE IF NOT EXISTS carts
(cart_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
item_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
added_date TEXT,
FOREIGN KEY (item_id) REFERENCES items(item_id))''')
conn.commit()
conn.close()
logger.info("Inventory: Database initialized successfully")
return True
except Exception as e:
logger.error(f"Inventory: Failed to initialize database: {e}")
return False
def round_price(amount, is_taxed_sale=False):
"""Round price based on penny rounding settings"""
if not disable_penny:
return float(Decimal(str(amount)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))
# Penny rounding logic
decimal_amount = Decimal(str(amount))
if is_taxed_sale:
# Round up for taxed sales
return float(decimal_amount.quantize(Decimal('0.05'), rounding=ROUND_HALF_UP))
else:
# Round down for cash sales
return float(decimal_amount.quantize(Decimal('0.05'), rounding=ROUND_DOWN))
def add_item(name, price, quantity=0, location=""):
"""Add a new item to inventory"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
current_date = time.strftime("%Y-%m-%d")
try:
# Check if item already exists
c.execute("SELECT item_id FROM items WHERE item_name = ?", (name,))
existing = c.fetchone()
if existing:
conn.close()
return f"Item '{name}' already exists. Use itemreset to update."
c.execute("""INSERT INTO items (item_name, item_price, item_quantity, location, created_date, updated_date)
VALUES (?, ?, ?, ?, ?, ?)""",
(name, price, quantity, location, current_date, current_date))
conn.commit()
conn.close()
return f"✅ Item added: {name} - ${price:.2f} - Qty: {quantity}"
except sqlite3.OperationalError as e:
if "no such table" in str(e):
initialize_inventory_database()
return add_item(name, price, quantity, location)
else:
conn.close()
logger.error(f"Inventory: Error adding item: {e}")
return "Error adding item."
except Exception as e:
conn.close()
logger.error(f"Inventory: Error adding item: {e}")
return "Error adding item."
def remove_item(name):
"""Remove an item from inventory"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
try:
c.execute("DELETE FROM items WHERE item_name = ?", (name,))
if c.rowcount == 0:
conn.close()
return f"Item '{name}' not found."
conn.commit()
conn.close()
return f"🗑️ Item removed: {name}"
except Exception as e:
conn.close()
logger.error(f"Inventory: Error removing item: {e}")
return "Error removing item."
def reset_item(name, price=None, quantity=None):
"""Update item price or quantity"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
current_date = time.strftime("%Y-%m-%d")
try:
# Check if item exists
c.execute("SELECT item_price, item_quantity FROM items WHERE item_name = ?", (name,))
item = c.fetchone()
if not item:
conn.close()
return f"Item '{name}' not found."
updates = []
params = []
if price is not None:
updates.append("item_price = ?")
params.append(price)
if quantity is not None:
updates.append("item_quantity = ?")
params.append(quantity)
if not updates:
conn.close()
return "No updates specified."
updates.append("updated_date = ?")
params.append(current_date)
params.append(name)
query = f"UPDATE items SET {', '.join(updates)} WHERE item_name = ?"
c.execute(query, params)
conn.commit()
conn.close()
update_msg = []
if price is not None:
update_msg.append(f"Price: ${price:.2f}")
if quantity is not None:
update_msg.append(f"Qty: {quantity}")
return f"🔄 Item updated: {name} - {' - '.join(update_msg)}"
except Exception as e:
conn.close()
logger.error(f"Inventory: Error resetting item: {e}")
return "Error updating item."
def sell_item(name, quantity, user_name="", notes=""):
"""Sell an item (remove from inventory and record transaction)"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
current_date = time.strftime("%Y-%m-%d")
current_time = time.strftime("%H:%M:%S")
try:
# Get item details
c.execute("SELECT item_id, item_price, item_quantity FROM items WHERE item_name = ?", (name,))
item = c.fetchone()
if not item:
conn.close()
return f"Item '{name}' not found."
item_id, price, current_qty = item
if current_qty < quantity:
conn.close()
return f"Insufficient quantity. Available: {current_qty}"
# Calculate total with rounding
total = round_price(price * quantity, is_taxed_sale=True)
# Create transaction
c.execute("""INSERT INTO transactions (transaction_type, transaction_date, transaction_time,
user_name, total_amount, notes)
VALUES (?, ?, ?, ?, ?, ?)""",
("SALE", current_date, current_time, user_name, total, notes))
transaction_id = c.lastrowid
# Add transaction item
c.execute("""INSERT INTO transaction_items (transaction_id, item_id, quantity, price_at_sale)
VALUES (?, ?, ?, ?)""",
(transaction_id, item_id, quantity, price))
# Update inventory
c.execute("UPDATE items SET item_quantity = item_quantity - ?, updated_date = ? WHERE item_id = ?",
(quantity, current_date, item_id))
conn.commit()
conn.close()
return f"💰 Sale: {quantity}x {name} - Total: ${total:.2f}"
except Exception as e:
conn.close()
logger.error(f"Inventory: Error selling item: {e}")
return "Error processing sale."
def return_item(transaction_id):
"""Return items from a transaction (reverse the sale)"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
current_date = time.strftime("%Y-%m-%d")
try:
# Get transaction details
c.execute("SELECT total_amount FROM transactions WHERE transaction_id = ?", (transaction_id,))
transaction = c.fetchone()
if not transaction:
conn.close()
return f"Transaction {transaction_id} not found."
# Get items in transaction
c.execute("""SELECT ti.item_id, ti.quantity, i.item_name
FROM transaction_items ti
JOIN items i ON ti.item_id = i.item_id
WHERE ti.transaction_id = ?""", (transaction_id,))
items = c.fetchall()
if not items:
conn.close()
return f"No items found for transaction {transaction_id}."
# Return items to inventory
for item_id, quantity, item_name in items:
c.execute("UPDATE items SET item_quantity = item_quantity + ?, updated_date = ? WHERE item_id = ?",
(quantity, current_date, item_id))
# Mark transaction as returned (or delete it)
c.execute("DELETE FROM transactions WHERE transaction_id = ?", (transaction_id,))
c.execute("DELETE FROM transaction_items WHERE transaction_id = ?", (transaction_id,))
conn.commit()
conn.close()
return f"↩️ Transaction {transaction_id} reversed. Items returned to inventory."
except Exception as e:
conn.close()
logger.error(f"Inventory: Error returning item: {e}")
return "Error processing return."
def list_items():
"""List all items in inventory"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
try:
c.execute("SELECT item_name, item_price, item_quantity, location FROM items ORDER BY item_name")
items = c.fetchall()
conn.close()
if not items:
return "No items in inventory."
result = "📦 Inventory:\n"
total_value = 0
for name, price, qty, location in items:
value = price * qty
total_value += value
loc_str = f" @ {location}" if location else ""
result += f"{name}: ${price:.2f} x {qty}{loc_str} = ${value:.2f}\n"
result += f"\nTotal Value: ${total_value:.2f}"
return result.rstrip()
except Exception as e:
conn.close()
logger.error(f"Inventory: Error listing items: {e}")
return "Error listing items."
def get_stats():
"""Get sales statistics"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
try:
current_date = time.strftime("%Y-%m-%d")
# Get today's sales
c.execute("""SELECT COUNT(*), SUM(total_amount)
FROM transactions
WHERE transaction_type = 'SALE' AND transaction_date = ?""",
(current_date,))
today_stats = c.fetchone()
today_count = today_stats[0] or 0
today_total = today_stats[1] or 0
# Get hot item (most sold today)
c.execute("""SELECT i.item_name, SUM(ti.quantity) as total_qty
FROM transaction_items ti
JOIN transactions t ON ti.transaction_id = t.transaction_id
JOIN items i ON ti.item_id = i.item_id
WHERE t.transaction_date = ? AND t.transaction_type = 'SALE'
GROUP BY i.item_name
ORDER BY total_qty DESC
LIMIT 1""", (current_date,))
hot_item = c.fetchone()
conn.close()
result = f"📊 Today's Stats:\n"
result += f"Sales: {today_count}\n"
result += f"Revenue: ${today_total:.2f}\n"
if hot_item:
result += f"Hot Item: {hot_item[0]} ({hot_item[1]} sold)"
else:
result += "Hot Item: None"
return result
except Exception as e:
conn.close()
logger.error(f"Inventory: Error getting stats: {e}")
return "Error getting stats."
def add_to_cart(user_id, item_name, quantity):
"""Add item to user's cart"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
current_date = time.strftime("%Y-%m-%d")
try:
# Get item details
c.execute("SELECT item_id, item_quantity FROM items WHERE item_name = ?", (item_name,))
item = c.fetchone()
if not item:
conn.close()
return f"Item '{item_name}' not found."
item_id, available_qty = item
# Check if item already in cart
c.execute("SELECT quantity FROM carts WHERE user_id = ? AND item_id = ?", (user_id, item_id))
existing = c.fetchone()
if existing:
new_qty = existing[0] + quantity
if new_qty > available_qty:
conn.close()
return f"Insufficient quantity. Available: {available_qty}"
c.execute("UPDATE carts SET quantity = ? WHERE user_id = ? AND item_id = ?",
(new_qty, user_id, item_id))
else:
if quantity > available_qty:
conn.close()
return f"Insufficient quantity. Available: {available_qty}"
c.execute("INSERT INTO carts (user_id, item_id, quantity, added_date) VALUES (?, ?, ?, ?)",
(user_id, item_id, quantity, current_date))
conn.commit()
conn.close()
return f"🛒 Added to cart: {quantity}x {item_name}"
except Exception as e:
conn.close()
logger.error(f"Inventory: Error adding to cart: {e}")
return "Error adding to cart."
def remove_from_cart(user_id, item_name):
"""Remove item from user's cart"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
try:
c.execute("""DELETE FROM carts
WHERE user_id = ? AND item_id = (SELECT item_id FROM items WHERE item_name = ?)""",
(user_id, item_name))
if c.rowcount == 0:
conn.close()
return f"Item '{item_name}' not in cart."
conn.commit()
conn.close()
return f"🗑️ Removed from cart: {item_name}"
except Exception as e:
conn.close()
logger.error(f"Inventory: Error removing from cart: {e}")
return "Error removing from cart."
def list_cart(user_id):
"""List items in user's cart"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
try:
c.execute("""SELECT i.item_name, i.item_price, c.quantity
FROM carts c
JOIN items i ON c.item_id = i.item_id
WHERE c.user_id = ?""", (user_id,))
items = c.fetchall()
conn.close()
if not items:
return "🛒 Cart is empty."
result = "🛒 Your Cart:\n"
total = 0
for name, price, qty in items:
subtotal = price * qty
total += subtotal
result += f"{name}: ${price:.2f} x {qty} = ${subtotal:.2f}\n"
total = round_price(total, is_taxed_sale=True)
result += f"\nTotal: ${total:.2f}"
return result
except Exception as e:
conn.close()
logger.error(f"Inventory: Error listing cart: {e}")
return "Error listing cart."
def checkout_cart(user_id, user_name="", transaction_type="SALE", notes=""):
"""Process cart as a transaction"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
current_date = time.strftime("%Y-%m-%d")
current_time = time.strftime("%H:%M:%S")
try:
# Get cart items
c.execute("""SELECT i.item_id, i.item_name, i.item_price, c.quantity, i.item_quantity
FROM carts c
JOIN items i ON c.item_id = i.item_id
WHERE c.user_id = ?""", (user_id,))
cart_items = c.fetchall()
if not cart_items:
conn.close()
return "Cart is empty."
# Verify all items have sufficient quantity
for item_id, name, price, cart_qty, stock_qty in cart_items:
if stock_qty < cart_qty:
conn.close()
return f"Insufficient quantity for '{name}'. Available: {stock_qty}"
# Calculate total
total = sum(price * qty for _, _, price, qty, _ in cart_items)
total = round_price(total, is_taxed_sale=(transaction_type == "SALE"))
# Create transaction
c.execute("""INSERT INTO transactions (transaction_type, transaction_date, transaction_time,
user_name, total_amount, notes)
VALUES (?, ?, ?, ?, ?, ?)""",
(transaction_type, current_date, current_time, user_name, total, notes))
transaction_id = c.lastrowid
# Process each item
for item_id, name, price, quantity, _ in cart_items:
# Add to transaction items
c.execute("""INSERT INTO transaction_items (transaction_id, item_id, quantity, price_at_sale)
VALUES (?, ?, ?, ?)""",
(transaction_id, item_id, quantity, price))
# Update inventory (subtract for SALE, add for BUY)
if transaction_type == "SALE":
c.execute("UPDATE items SET item_quantity = item_quantity - ?, updated_date = ? WHERE item_id = ?",
(quantity, current_date, item_id))
else: # BUY
c.execute("UPDATE items SET item_quantity = item_quantity + ?, updated_date = ? WHERE item_id = ?",
(quantity, current_date, item_id))
# Clear cart
c.execute("DELETE FROM carts WHERE user_id = ?", (user_id,))
conn.commit()
conn.close()
emoji = "💰" if transaction_type == "SALE" else "📦"
return f"{emoji} Transaction #{transaction_id} completed: ${total:.2f}"
except Exception as e:
conn.close()
logger.error(f"Inventory: Error processing cart: {e}")
return "Error processing cart."
def clear_cart(user_id):
"""Clear user's cart"""
conn = sqlite3.connect(inventory_db)
c = conn.cursor()
try:
c.execute("DELETE FROM carts WHERE user_id = ?", (user_id,))
conn.commit()
conn.close()
return "🗑️ Cart cleared."
except Exception as e:
conn.close()
logger.error(f"Inventory: Error clearing cart: {e}")
return "Error clearing cart."
def process_inventory_command(nodeID, message, name="none"):
"""Process inventory and POS commands"""
# Check ban list
if str(nodeID) in bbs_ban_list:
logger.warning("System: Inventory attempt from the ban list")
return "Unable to process command"
message_lower = message.lower()
parts = message.split()
try:
# Help command
if "?" in message_lower:
return get_inventory_help()
# Item management commands
if message_lower.startswith("itemadd "):
# itemadd name price quantity [location]
if len(parts) < 4:
return "Usage: itemadd <name> <price> <quantity> [location]"
item_name = parts[1]
try:
price = float(parts[2])
quantity = int(parts[3])
location = " ".join(parts[4:]) if len(parts) > 4 else ""
return add_item(item_name, price, quantity, location)
except ValueError:
return "Invalid price or quantity."
elif message_lower.startswith("itemremove "):
item_name = " ".join(parts[1:])
return remove_item(item_name)
elif message_lower.startswith("itemreset "):
# itemreset name [price=X] [quantity=Y]
if len(parts) < 2:
return "Usage: itemreset <name> [price=X] [quantity=Y]"
item_name = parts[1]
price = None
quantity = None
for part in parts[2:]:
if part.startswith("price="):
try:
price = float(part.split("=")[1])
except ValueError:
return "Invalid price value."
elif part.startswith("quantity=") or part.startswith("qty="):
try:
quantity = int(part.split("=")[1])
except ValueError:
return "Invalid quantity value."
return reset_item(item_name, price, quantity)
elif message_lower.startswith("itemsell "):
# itemsell name quantity [notes]
if len(parts) < 3:
return "Usage: itemsell <name> <quantity> [notes]"
item_name = parts[1]
try:
quantity = int(parts[2])
notes = " ".join(parts[3:]) if len(parts) > 3 else ""
return sell_item(item_name, quantity, name, notes)
except ValueError:
return "Invalid quantity."
elif message_lower.startswith("itemreturn "):
# itemreturn transaction_id
if len(parts) < 2:
return "Usage: itemreturn <transaction_id>"
try:
transaction_id = int(parts[1])
return return_item(transaction_id)
except ValueError:
return "Invalid transaction ID."
elif message_lower == "itemlist":
return list_items()
elif message_lower == "itemstats":
return get_stats()
# Cart commands
elif message_lower.startswith("cartadd "):
# cartadd name quantity
if len(parts) < 3:
return "Usage: cartadd <name> <quantity>"
item_name = parts[1]
try:
quantity = int(parts[2])
return add_to_cart(str(nodeID), item_name, quantity)
except ValueError:
return "Invalid quantity."
elif message_lower.startswith("cartremove "):
item_name = " ".join(parts[1:])
return remove_from_cart(str(nodeID), item_name)
elif message_lower == "cartlist" or message_lower == "cart":
return list_cart(str(nodeID))
elif message_lower.startswith("cartbuy") or message_lower.startswith("cartsell"):
transaction_type = "BUY" if "buy" in message_lower else "SALE"
notes = " ".join(parts[1:]) if len(parts) > 1 else ""
return checkout_cart(str(nodeID), name, transaction_type, notes)
elif message_lower == "cartclear":
return clear_cart(str(nodeID))
else:
return "Invalid command. Send 'item?' for help."
except Exception as e:
logger.error(f"Inventory: Error processing command: {e}")
return "Error processing command."
def get_inventory_help():
"""Return help text for inventory commands"""
return """📦 Inventory Commands:
itemadd <name> <price> <qty> [loc]
itemremove <name>
itemreset <name> [price=X] [qty=Y]
itemsell <name> <qty> [notes]
itemreturn <transaction_id>
itemlist - list inventory
itemstats - today's stats
🛒 Cart Commands:
cartadd <name> <qty>
cartremove <name>
cartlist - view cart
cartbuy/cartsell [notes]
cartclear - empty cart"""
+9
View File
@@ -125,6 +125,10 @@ if 'qrz' not in config:
config['qrz'] = {'enabled': 'False', 'qrz_db': 'data/qrz.db', 'qrz_hello_string': 'send CMD or DM me for more info.'}
config.write(open(config_file, 'w'))
if 'inventory' not in config:
config['inventory'] = {'enabled': 'False', 'inventory_db': 'data/inventory.db', 'disable_penny': 'False'}
config.write(open(config_file, 'w'))
# interface1 settings
interface1_type = config['interface'].get('type', 'serial')
port1 = config['interface'].get('port', '')
@@ -356,6 +360,11 @@ try:
qrz_hello_string = config['qrz'].get('qrz_hello_string', 'MeshBot says Hello! DM for more info.')
train_qrz = config['qrz'].getboolean('training', True)
# inventory and POS
inventory_enabled = config['inventory'].getboolean('enabled', False)
inventory_db = config['inventory'].get('inventory_db', 'data/inventory.db')
disable_penny = config['inventory'].getboolean('disable_penny', False)
# E-Mail Settings
sysopEmails = config['smtp'].get('sysopEmails', '').split(',')
enableSMTP = config['smtp'].getboolean('enableSMTP', False)