from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
from flask_sqlalchemy import SQLAlchemy
import os
import logging
# Configure logging
logging.basicConfig(filename='app.log', level=logging.ERROR, format='%(asctime)s %(levelname)s: %(message)s')
app = Flask(__name__)
app.secret_key = 'seedvault_secret_key'
basedir = os.path.abspath(os.path.dirname(__file__))
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'inventory.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
# --- Models ---
class Product(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
sku = db.Column(db.String(50), nullable=True)
bulk_weight_g = db.Column(db.Float, default=0.0)
weight_per_packet_g = db.Column(db.Float, nullable=False)
low_stock_threshold_g = db.Column(db.Float, default=10.0)
current_stock_g = db.Column(db.Float, default=0.0)
product_type = db.Column(db.String(20), default='weight')
current_stock_qty = db.Column(db.Integer, default=0)
is_unit_based = db.Column(db.Boolean, default=False)
def get_status(self):
# For unit-based, we check qty. For weight-based, we check grams.
stock_val = self.current_stock_qty if self.is_unit_based else self.current_stock_g
threshold_val = 1.0 if self.is_unit_based else self.low_stock_threshold_g
if stock_val <= 0:
return 'OUT'
if stock_val <= threshold_val:
return 'LOW'
return 'OK'
class SyncLog(db.Model):
__tablename__ = 'sync_log'
id = db.Column(db.Integer, primary_key=True)
timestamp = db.Column(db.DateTime, default=db.func.current_timestamp)
status = db.Column(db.String(20)) # 'success' or 'error'
message = db.Column(db.Text)
# --- Routes ---
@app.route('/')
def index():
search_query = request.args.get('search', '').strip()
filter_type = request.args.get('filter', 'all')
products = Product.query
if search_query:
products = products.filter(
(Product.name.contains(search_query)) |
(Product.sku.contains(search_query))
)
if filter_type == 'action_required':
# Filter for products that are LOW or OUT
# Since get_status() is a Python method, we can't use it in a SQL filter directly.
# We have to do it in Python or use the underlying logic in SQL.
# Logic: (is_unit_based AND current_stock_qty <= threshold) OR (NOT is_unit_based AND current_stock_g <= threshold)
# But threshold is different for unit vs weight.
# Let's just fetch and filter in Python for simplicity, or use a more complex SQL query.
# Given the small number of products, Python filtering is fine.
pass
products_list = products.all()
if filter_type == 'action_required':
products_list = [p for p in products_list if p.get_status() in ['LOW', 'OUT']]
last_sync = SyncLog.query.order_by(SyncLog.timestamp.desc()).first()
return render_template('index.html',
products=products_list,
search_query=search_query,
filter_type=filter_type,
last_sync=last_sync)
@app.route('/add_product', methods=['POST'])
def add_product():
name = request.form.get('name')
sku = request.form.get('sku') or None
is_unit_based = request.form.get('is_unit_based') == 'on'
weight_per_packet = float(request.form.get('weight_per_packet')) if not is_unit_based else 1.0
initial_stock = float(request.form.get('current_stock'))
threshold = float(request.form.get('threshold'))
stock_g = 0.0
stock_qty = 0
if is_unit_based:
stock_qty = int(initial_stock)
else:
stock_g = initial_stock
new_product = Product(
name=name,
sku=sku,
bulk_weight_g=0.0,
weight_per_packet_g=weight_per_packet,
current_stock_g=stock_g,
current_stock_qty=stock_qty,
low_stock_threshold_g=threshold,
is_unit_based=is_unit_based,
product_type='quantity' if is_unit_based else 'weight'
)
try:
db.session.add(new_product)
db.session.commit()
flash(f'Product "{name}" added successfully!', 'success')
except Exception as e:
db.session.rollback()
flash(f'Error adding product: {str(e)}', 'danger')
return redirect(url_for('index'))
@app.route('/edit/<int:product_id>', methods=['GET', 'POST'])
def edit_product(product_id):
product = Product.query.get_or_404(product_id)
if request.method == 'POST':
try:
product.name = request.form.get('name')
product.sku = request.form.get('sku') or None
product.is_unit_based = request.form.get('is_unit_based') == 'on'
if product.is_unit_based:
product.weight_per_packet_g = 1.0
else:
product.weight_per_packet_g = float(request.form.get('weight_per_packet'))
initial_stock = float(request.form.get('current_stock'))
product.low_stock_threshold_g = float(request.form.get('threshold'))
if product.is_unit_based:
product.current_stock_qty = int(initial_stock)
product.current_stock_g = 0.0
product.product_type = 'quantity'
else:
product.current_stock_g = initial_stock
product.current_stock_qty = 0
product.product_type = 'weight'
db.session.commit()
flash(f'Product "{product.name}" updated successfully!', 'success')
return redirect(url_for('index'))
except Exception as e:
db.session.rollback()
logging.error(f"Error updating product {product_id}: {str(e)}", exc_info=True)
flash(f'Error updating product: {str(e)}', 'danger')
return render_template('edit.html', product=product)
@app.route('/restock', methods=['POST'])
def restock():
product_id = request.form.get('product_id')
amount = float(request.form.get('amount'))
product = Product.query.get(product_id)
if product:
if product.is_unit_based:
# If unit based, amount is the number of units.
product.current_stock_qty += int(amount)
else:
# If weighted, amount is packets, so add amount * weight.
product.current_stock_g += (amount * product.weight_per_packet_g)
db.session.commit()
flash(f'Restocked {product.name}.', 'success')
else:
flash('Product not found.', 'danger')
return redirect(url_for('index'))
@app.route('/sell', methods=['POST'])
def sell():
product_id = request.form.get('product_id')
packets = int(request.form.get('packets'))
product = Product.query.get(product_id)
if product:
if product.is_unit_based:
# If unit based, deduction is just the number of units.
deduction = packets
if product.current_stock_qty >= deduction:
product.current_stock_qty -= deduction
db.session.commit()
flash(f'Sold {packets} packets of {product.name}.', 'success')
else:
flash(f'Insufficient stock for {product.name}!', 'danger')
else:
# If weighted, deduction is packets * weight.
deduction = packets * product.weight_per_packet_g
if product.current_stock_g >= deduction:
product.current_stock_g -= deduction
db.session.commit()
flash(f'Sold {packets} packets of {product.name}.', 'success')
else:
flash(f'Insufficient bulk stock for {product.name}!', 'danger')
else:
flash('Product not found.', 'danger')
return redirect(url_for('index'))
@app.route('/delete/<int:product_id>', methods=['POST'])
def delete_product(product_id):
product = Product.query.get(product_id)
if product:
db.session.delete(product)
db.session.commit()
flash(f'Deleted {product.name}.', 'warning')
return redirect(url_for('index'))
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(host='127.0.0.1', port=5001, debug=True)