modified: Dockerfile

modified:   README.md
	modified:   app.py
	new file:   blueprints/__init__.py
	new file:   blueprints/__pycache__/.gitignore
	new file:   blueprints/auth.py
	new file:   blueprints/finance.py
	new file:   blueprints/inventory.py
	new file:   blueprints/pos.py
	new file:   blueprints/sales.py
	new file:   core/__pycache__/.gitignore
	new file:   core/db.py
	new file:   core/db/.gitignore
	new file:   core/events.py
	new file:   core/openfood.py
	new file:   core/utils.py
	modified:   static/style.css
	modified:   templates/checkout.html
	modified:   templates/dicom.html
	modified:   templates/login.html
	modified:   templates/macros/base.html
	modified:   templates/macros/modals.html
	modified:   templates/macros/navbar.html
This commit is contained in:
2026-05-21 00:05:31 -04:00
parent c2373c3ed6
commit a5babd8131
23 changed files with 2102 additions and 1169 deletions

1
blueprints/__init__.py Normal file
View File

@@ -0,0 +1 @@
# blueprints/__init__.py

2
blueprints/__pycache__/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

60
blueprints/auth.py Normal file
View File

@@ -0,0 +1,60 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
from werkzeug.security import check_password_hash, generate_password_hash
from core.db import get_db_connection
auth_bp = Blueprint('auth', __name__)
login_manager = LoginManager()
login_manager.login_view = 'auth.login'
class User(UserMixin):
def __init__(self, id, username):
self.id = id
self.username = username
@login_manager.user_loader
def load_user(user_id):
with get_db_connection() as conn:
user = conn.execute('SELECT id, username FROM users WHERE id = ?', (user_id,)).fetchone()
return User(user[0], user[1]) if user else None
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
user_in = request.form.get('username')
pass_in = request.form.get('password')
with get_db_connection() as conn:
user = conn.execute('SELECT * FROM users WHERE username = ?', (user_in,)).fetchone()
if user and check_password_hash(user[2], pass_in):
login_user(User(user[0], user[1]))
return redirect(url_for('inventory.inventory'))
flash('Invalid credentials.')
return render_template('login.html')
@auth_bp.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('auth.login'))
@auth_bp.route('/settings/update', methods=['POST'])
@login_required
def update_settings():
new_password = request.form.get('password')
profile_pic = request.form.get('profile_pic')
with get_db_connection() as conn:
if new_password and len(new_password) > 0:
hashed_pw = generate_password_hash(new_password)
conn.execute('UPDATE users SET password = ? WHERE id = ?', (hashed_pw, current_user.id))
if profile_pic:
conn.execute('UPDATE users SET profile_pic = ? WHERE id = ?', (profile_pic, current_user.id))
conn.commit()
flash('Configuración actualizada')
return redirect(request.referrer)
def init_login_manager(app):
login_manager.init_app(app)

398
blueprints/finance.py Normal file
View File

@@ -0,0 +1,398 @@
from flask import Blueprint, render_template, request, jsonify
from flask_login import login_required, current_user
from core.db import get_db_connection
finance_bp = Blueprint('finance', __name__)
@finance_bp.route('/dicom')
@login_required
def dicom():
with get_db_connection() as conn:
debtors = conn.execute('''SELECT d.id, d.name, d.contact_info,
COALESCE(SUM(t.total - t.amount_paid), 0) as total_balance
FROM debtors d
LEFT JOIN debtor_tickets t ON d.id = t.debtor_id
GROUP BY d.id
ORDER BY total_balance DESC''').fetchall()
return render_template('dicom.html', active_page='dicom', user=current_user, debtors=debtors)
@finance_bp.route('/api/dicom/debtor/<int:debtor_id>', methods=['GET'])
@login_required
def get_debtor_details(debtor_id):
with get_db_connection() as conn:
# Get tickets with their remaining balance
tickets = conn.execute('''SELECT id, date, total, amount_paid, status,
total - amount_paid as remaining
FROM debtor_tickets
WHERE debtor_id = ?
ORDER BY date DESC''', (debtor_id,)).fetchall()
# Get items for each ticket
result = []
for t in tickets:
items = conn.execute('''SELECT id, barcode, name, price, quantity, subtotal
FROM debtor_ticket_items
WHERE ticket_id = ?''', (t[0],)).fetchall()
result.append({
"id": t[0],
"date": t[1],
"total": t[2],
"amount_paid": t[3],
"status": t[4],
"remaining": t[5],
"items": [{"id": i[0], "barcode": i[1], "name": i[2], "price": i[3], "qty": i[4], "subtotal": i[5]} for i in items]
})
return jsonify(result)
@finance_bp.route('/api/dicom/debtor/<int:debtor_id>/pay', methods=['POST'])
@login_required
def pay_debtor_ticket(debtor_id):
data = request.get_json()
ticket_id = data.get('ticket_id')
amount = float(data.get('amount', 0))
if not ticket_id or amount <= 0:
return jsonify({"error": "Monto inválido"}), 400
with get_db_connection() as conn:
conn.execute('''UPDATE debtor_tickets
SET amount_paid = amount_paid + ?,
status = CASE WHEN (total - amount_paid - ?) <= 0 THEN 'paid' ELSE 'partial' END
WHERE id = ?''', (amount, amount, ticket_id))
# Update status based on final values
conn.execute('''UPDATE debtor_tickets
SET status = CASE
WHEN total - amount_paid <= 0 THEN 'paid'
WHEN amount_paid > 0 THEN 'partial'
ELSE 'unpaid'
END
WHERE id = ?''', (ticket_id,))
conn.commit()
return jsonify({"status": "success"})
@finance_bp.route('/api/dicom/pay', methods=['POST'])
@login_required
def dicom_pay():
data = request.get_json()
ticket_id = data.get('ticket_id')
amount = float(data.get('amount', 0))
payment_method = data.get('payment_method', 'efectivo')
if not ticket_id or amount <= 0:
return jsonify({"error": "Monto inválido"}), 400
try:
with get_db_connection() as conn:
cur = conn.cursor()
# Update the debtor ticket payment
cur.execute('UPDATE debtor_tickets SET amount_paid = amount_paid + ? WHERE id = ?', (amount, ticket_id))
# Update status based on final values
cur.execute('''UPDATE debtor_tickets
SET status = CASE
WHEN total - amount_paid <= 0 THEN 'paid'
WHEN amount_paid > 0 THEN 'partial'
ELSE 'unpaid'
END
WHERE id = ?''', (ticket_id,))
# Insert into sales table to track daily revenue
cur.execute('''INSERT INTO sales (date, total, payment_method)
VALUES (CURRENT_TIMESTAMP, ?, ?)''', (amount, payment_method))
conn.commit()
return jsonify({"status": "success", "amount": amount}), 200
except Exception as e:
print(f"Dicom Pay Error: {e}")
return jsonify({"error": str(e)}), 500
@finance_bp.route('/api/dicom/update', methods=['POST'])
@login_required
def update_dicom():
data = request.get_json()
name = data.get('name', '').strip()
amount = float(data.get('amount', 0))
notes = data.get('notes', '')
image_url = data.get('image_url', '')
action = data.get('action')
if not name or amount <= 0:
return jsonify({"error": "Nombre y monto válidos son requeridos"}), 400
if action == 'add':
amount = -amount
with get_db_connection() as conn:
cur = conn.cursor()
cur.execute('''INSERT INTO dicom (name, amount, notes, image_url, last_updated)
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(name) DO UPDATE SET
amount = amount + excluded.amount,
notes = excluded.notes,
image_url = CASE WHEN excluded.image_url != "" THEN excluded.image_url ELSE dicom.image_url END,
last_updated = CURRENT_TIMESTAMP''', (name, amount, notes, image_url))
conn.commit()
return jsonify({"status": "success"}), 200
@finance_bp.route('/api/dicom/<int:debtor_id>', methods=['DELETE'])
@login_required
def delete_dicom(debtor_id):
try:
with get_db_connection() as conn:
conn.execute('DELETE FROM dicom WHERE id = ?', (debtor_id,))
conn.commit()
return jsonify({"status": "success"}), 200
except Exception as e:
return jsonify({"error": str(e)}), 500
@finance_bp.route('/gastos')
@login_required
def gastos():
from datetime import datetime
selected_month = request.args.get('month', datetime.now().strftime('%Y-%m'))
with get_db_connection() as conn:
cur = conn.cursor()
cur.execute('''
CREATE TABLE IF NOT EXISTS expenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
description TEXT NOT NULL,
amount INTEGER NOT NULL
)
''')
sales_total = cur.execute("SELECT SUM(total) FROM sales WHERE strftime('%Y-%m', date, 'localtime') = ?", (selected_month,)).fetchone()[0] or 0
expenses_total = cur.execute("SELECT SUM(amount) FROM expenses WHERE strftime('%Y-%m', date, 'localtime') = ?", (selected_month,)).fetchone()[0] or 0
expenses_list = cur.execute("SELECT id, date, description, amount FROM expenses WHERE strftime('%Y-%m', date, 'localtime') = ? ORDER BY date DESC", (selected_month,)).fetchall()
return render_template('gastos.html',
active_page='gastos',
user=current_user,
sales_total=sales_total,
expenses_total=expenses_total,
net_profit=sales_total - expenses_total,
expenses=expenses_list,
selected_month=selected_month)
@finance_bp.route('/api/gastos', methods=['POST'])
@login_required
def add_gasto():
data = request.get_json()
desc = data.get('description')
amount = data.get('amount')
if not desc or not amount:
return jsonify({"error": "Faltan datos"}), 400
with get_db_connection() as conn:
cur = conn.cursor()
cur.execute("INSERT INTO expenses (description, amount) VALUES (?, ?)", (desc, int(amount)))
conn.commit()
return jsonify({"success": True})
@finance_bp.route('/api/gastos/<int:gasto_id>', methods=['DELETE'])
@login_required
def delete_gasto(gasto_id):
with get_db_connection() as conn:
cur = conn.cursor()
cur.execute("DELETE FROM expenses WHERE id = ?", (gasto_id,))
conn.commit()
return jsonify({"success": True})
@finance_bp.route('/api/dicom/debtors', methods=['GET'])
@login_required
def get_debtors():
try:
with get_db_connection() as conn:
# Check if table exists
cur = conn.cursor()
cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='debtors'")
if not cur.fetchone():
print("Debtors table does not exist!")
return jsonify([])
cur.execute('SELECT id, name, contact_info FROM debtors ORDER BY name')
debtors = cur.fetchall()
print(f"Found {len(debtors)} debtors:", debtors)
return jsonify([{"id": d[0], "name": d[1], "contact_info": d[2]} for d in debtors])
except Exception as e:
print(f"Error getting debtors: {e}")
import traceback
traceback.print_exc()
return jsonify([])
@finance_bp.route('/api/dicom/checkout', methods=['POST'])
@login_required
def dicom_checkout():
try:
data = request.get_json()
cart = data.get('cart', [])
debtor_name = data.get('debtor_name', '').strip()
contact_info = data.get('contact_info', '').strip()
initial_payment = data.get('initial_payment', 0) or 0
if not cart:
return jsonify({"error": "Carrito vacío"}), 400
if not debtor_name:
return jsonify({"error": "Nombre del deudor requerido"}), 400
total = sum(item.get('subtotal', 0) for item in cart)
with get_db_connection() as conn:
cur = conn.cursor()
# Upsert debtor
cur.execute('''INSERT INTO debtors (name, contact_info)
VALUES (?, ?)
ON CONFLICT(name) DO UPDATE SET
contact_info = excluded.contact_info''',
(debtor_name, contact_info))
# Get debtor ID
debtor_id = cur.execute('SELECT id FROM debtors WHERE name = ?', (debtor_name,)).fetchone()[0]
# Insert debtor ticket
status = 'partial' if initial_payment > 0 else 'unpaid'
cur.execute('''INSERT INTO debtor_tickets (debtor_id, total, amount_paid, status)
VALUES (?, ?, ?, ?)''',
(debtor_id, total, initial_payment, status))
ticket_id = cur.lastrowid
# Insert ticket items and deduct stock
for item in cart:
cur.execute('''INSERT INTO debtor_ticket_items
(ticket_id, barcode, name, price, quantity, subtotal)
VALUES (?, ?, ?, ?, ?, ?)''',
(ticket_id, item.get('barcode', ''), item.get('name'),
item.get('price'), item.get('qty'), item.get('subtotal')))
# Deduct stock (skip for manual products)
if item.get('barcode') and not item.get('barcode', '').startswith(('MANUAL-', 'VARIOS-', 'RAPIDA-')):
cur.execute('UPDATE products SET stock = stock - ? WHERE barcode = ?',
(item.get('qty'), item.get('barcode')))
conn.commit()
return jsonify({"status": "success", "ticket_id": ticket_id, "debtor": debtor_name}), 200
except Exception as e:
print(f"Dicom Checkout Error: {e}")
return jsonify({"error": str(e)}), 500
@finance_bp.route('/api/dicom/debtor/<int:debtor_id>', methods=['DELETE'])
@login_required
def delete_debtor(debtor_id):
try:
with get_db_connection() as conn:
cur = conn.cursor()
# Delete items first
cur.execute('DELETE FROM debtor_ticket_items WHERE ticket_id IN (SELECT id FROM debtor_tickets WHERE debtor_id = ?)', (debtor_id,))
# Delete tickets
cur.execute('DELETE FROM debtor_tickets WHERE debtor_id = ?', (debtor_id,))
# Delete debtor
cur.execute('DELETE FROM debtors WHERE id = ?', (debtor_id,))
conn.commit()
return jsonify({"status": "success"}), 200
except Exception as e:
print(f"Delete Debtor Error: {e}")
return jsonify({"error": str(e)}), 500
@finance_bp.route('/api/dicom/ticket/<int:ticket_id>', methods=['DELETE'])
@login_required
def delete_ticket(ticket_id):
try:
with get_db_connection() as conn:
cur = conn.cursor()
# Delete items first
cur.execute('DELETE FROM debtor_ticket_items WHERE ticket_id = ?', (ticket_id,))
# Delete ticket
cur.execute('DELETE FROM debtor_tickets WHERE id = ?', (ticket_id,))
conn.commit()
return jsonify({"status": "success"}), 200
except Exception as e:
print(f"Delete Ticket Error: {e}")
return jsonify({"error": str(e)}), 500
@finance_bp.route('/api/dicom/item/<int:item_id>', methods=['DELETE'])
@login_required
def delete_item(item_id):
try:
with get_db_connection() as conn:
cur = conn.cursor()
# Get item info to update ticket total
item = cur.execute('SELECT ticket_id, subtotal FROM debtor_ticket_items WHERE id = ?', (item_id,)).fetchone()
if not item:
return jsonify({"error": "Item no encontrado"}), 404
ticket_id, item_subtotal = item
# Delete item
cur.execute('DELETE FROM debtor_ticket_items WHERE id = ?', (item_id,))
# Check if ticket has remaining items
remaining_items = cur.execute('SELECT COUNT(*) FROM debtor_ticket_items WHERE ticket_id = ?', (ticket_id,)).fetchone()[0]
if remaining_items == 0:
# Delete ticket if no items left
cur.execute('DELETE FROM debtor_tickets WHERE id = ?', (ticket_id,))
return jsonify({"status": "success", "ticket_deleted": True}), 200
# Update ticket total
cur.execute('UPDATE debtor_tickets SET total = total - ? WHERE id = ?', (item_subtotal, ticket_id))
conn.commit()
return jsonify({"status": "success", "ticket_deleted": False}), 200
except Exception as e:
print(f"Delete Item Error: {e}")
return jsonify({"error": str(e)}), 500
@finance_bp.route('/api/dicom/debtor/<int:debtor_id>/pay-all', methods=['POST'])
@login_required
def pay_all_debtor(debtor_id):
try:
data = request.get_json()
amount = float(data.get('amount', 0))
payment_method = data.get('payment_method', 'efectivo')
if amount <= 0:
return jsonify({"error": "Monto inválido"}), 400
with get_db_connection() as conn:
cur = conn.cursor()
# Get all unpaid/partial tickets for this debtor
tickets = cur.execute('''SELECT id, total, amount_paid, total - amount_paid as remaining
FROM debtor_tickets
WHERE debtor_id = ? AND status != 'paid' ''', (debtor_id,)).fetchall()
for ticket in tickets:
ticket_id = ticket[0]
remaining = ticket[3]
if remaining > 0:
# Pay remaining amount
cur.execute('UPDATE debtor_tickets SET amount_paid = amount_paid + ? WHERE id = ?',
(remaining, ticket_id))
# Update status
cur.execute('''UPDATE debtor_tickets SET status = 'paid' WHERE id = ?''', (ticket_id,))
# Record sale
cur.execute('''INSERT INTO sales (date, total, payment_method)
VALUES (CURRENT_TIMESTAMP, ?, ?)''', (remaining, payment_method))
conn.commit()
return jsonify({"status": "success"}), 200
except Exception as e:
print(f"Pay All Debtor Error: {e}")
return jsonify({"error": str(e)}), 500

112
blueprints/inventory.py Normal file
View File

@@ -0,0 +1,112 @@
import os
from flask import Blueprint, render_template, request, redirect, url_for, jsonify, current_app
from flask_login import login_required, current_user
from core.db import get_db_connection
from core.utils import download_image
from core.events import socketio
inventory_bp = Blueprint('inventory', __name__)
@inventory_bp.route('/inventory')
@login_required
def inventory():
with get_db_connection() as conn:
products = conn.execute('SELECT * FROM products').fetchall()
return render_template('inventory.html', active_page='inventory', products=products, user=current_user)
@inventory_bp.route("/upsert", methods=["POST"])
@login_required
def upsert():
d = request.form
barcode = d['barcode']
price_str = d.get('price', '0')
stock_str = d.get('stock', '0')
try:
price = float(price_str) if price_str else 0.0
stock = float(stock_str) if stock_str else 0.0
except (ValueError, TypeError):
price = 0.0
stock = 0.0
name = d.get('name', '')
image_url = d.get('image_url', '')
unit_type = d.get('unit_type', 'unit')
cache_dir = current_app.config['CACHE_DIR']
final_image_path = download_image(image_url, barcode, cache_dir)
with get_db_connection() as conn:
conn.execute('''INSERT INTO products (barcode, name, price, image_url, stock, unit_type)
VALUES (?,?,?,?,?,?)
ON CONFLICT(barcode) DO UPDATE SET
name=excluded.name,
price=excluded.price,
image_url=excluded.image_url,
stock=excluded.stock,
unit_type=excluded.unit_type''',
(barcode, name, price, final_image_path, stock, unit_type))
conn.commit()
return redirect(url_for('inventory.inventory'))
@inventory_bp.route('/delete/<barcode>', methods=['POST'])
@login_required
def delete(barcode):
cache_dir = current_app.config['CACHE_DIR']
with get_db_connection() as conn:
conn.execute('DELETE FROM products WHERE barcode = ?', (barcode,))
conn.commit()
img_p = os.path.join(cache_dir, f"{barcode}.jpg")
if os.path.exists(img_p): os.remove(img_p)
if socketio:
socketio.emit('product_deleted', {"barcode": barcode})
return redirect(url_for('inventory.inventory'))
@inventory_bp.route('/bulk_price_update', methods=['POST'])
@login_required
def bulk_price_update():
data = request.get_json()
barcodes = data.get('barcodes', [])
new_price = data.get('new_price')
if not barcodes or new_price is None:
return jsonify({"error": "Missing data"}), 400
try:
with get_db_connection() as conn:
params = [(float(new_price), b) for b in barcodes]
conn.executemany('UPDATE products SET price = ? WHERE barcode = ?', params)
conn.commit()
return jsonify({"status": "success"}), 200
except Exception as e:
print(f"Bulk update failed: {e}")
return jsonify({"error": str(e)}), 500
@inventory_bp.route('/bulk_delete', methods=['POST'])
@login_required
def bulk_delete():
cache_dir = current_app.config['CACHE_DIR']
data = request.get_json()
barcodes = data.get('barcodes', [])
if not barcodes:
return jsonify({"error": "No barcodes provided"}), 400
try:
with get_db_connection() as conn:
conn.execute(f'DELETE FROM products WHERE barcode IN ({",".join(["?"]*len(barcodes))})', barcodes)
conn.commit()
for barcode in barcodes:
img_p = os.path.join(cache_dir, f"{barcode}.jpg")
if os.path.exists(img_p):
os.remove(img_p)
return jsonify({"status": "success"}), 200
except Exception as e:
print(f"Bulk delete failed: {e}")
return jsonify({"error": str(e)}), 500

115
blueprints/pos.py Normal file
View File

@@ -0,0 +1,115 @@
import os
import time
from flask import Blueprint, render_template, request, jsonify, current_app
from flask_login import login_required, current_user
from core.db import get_db_connection
from core.openfood import fetch_from_openfoodfacts
from core.events import socketio
pos_bp = Blueprint('pos', __name__)
@pos_bp.route('/checkout')
@login_required
def checkout():
with get_db_connection() as conn:
products = conn.execute('SELECT barcode, name, price, image_url, stock, unit_type FROM products').fetchall()
return render_template("checkout.html", active_page='checkout', user=current_user, products=products)
@pos_bp.route('/scan', methods=['GET'])
def scan():
barcode = request.args.get('content', '').replace('{content}', '')
if not barcode:
return jsonify({"status": "error", "message": "empty barcode"}), 400
with get_db_connection() as conn:
p = conn.execute('SELECT barcode, name, price, image_url, stock, unit_type FROM products WHERE barcode = ?', (barcode,)).fetchone()
if p:
barcode_val, name, price, image_path, stock, unit_type = p
if image_path and image_path.startswith('/static/'):
clean_path = image_path.split('?')[0].lstrip('/')
if not os.path.exists(clean_path):
cache_dir = current_app.config['CACHE_DIR']
ext_data = fetch_from_openfoodfacts(barcode_val, cache_dir)
if ext_data and ext_data.get('image'):
image_path = ext_data['image']
with get_db_connection() as conn:
conn.execute('UPDATE products SET image_url = ? WHERE barcode = ?', (image_path, barcode_val))
conn.commit()
product_data = {
"barcode": barcode_val,
"name": name,
"price": int(price),
"image": image_path,
"stock": stock,
"unit_type": unit_type
}
socketio.emit('new_scan', product_data)
return jsonify({"status": "ok", "data": product_data}), 200
cache_dir = current_app.config['CACHE_DIR']
ext = fetch_from_openfoodfacts(barcode, cache_dir)
if ext:
external_data = {
"barcode": barcode,
"name": ext['name'],
"image": ext['image'],
"source": "openfoodfacts"
}
socketio.emit('scan_error', external_data)
return jsonify({"status": "not_found", "data": external_data}), 404
socketio.emit('scan_error', {"barcode": barcode})
return jsonify({"status": "not_found", "data": {"barcode": barcode}}), 404
@pos_bp.route('/api/checkout', methods=['POST'])
@login_required
def process_checkout():
try:
data = request.get_json()
cart = data.get('cart', [])
payment_method = data.get('payment_method', 'efectivo')
if not cart:
return jsonify({"error": "Cart is empty"}), 400
total = sum(item.get('subtotal', 0) for item in cart)
with get_db_connection() as conn:
cur = conn.cursor()
cur.execute('INSERT INTO sales (date, total, payment_method) VALUES (CURRENT_TIMESTAMP, ?, ?)', (total, payment_method))
sale_id = cur.lastrowid
for item in cart:
cur.execute('''INSERT INTO sale_items (sale_id, barcode, name, price, quantity, subtotal)
VALUES (?, ?, ?, ?, ?, ?)''',
(sale_id, item['barcode'], item['name'], item['price'], item['qty'], item['subtotal']))
cur.execute('UPDATE products SET stock = stock - ? WHERE barcode = ?', (item['qty'], item['barcode']))
conn.commit()
return jsonify({"status": "success", "sale_id": sale_id}), 200
except Exception as e:
print(f"Checkout Error: {e}")
return jsonify({"error": str(e)}), 500
@pos_bp.route('/api/scale/weight', methods=['POST'])
def update_scale_weight():
data = request.get_json()
weight_grams = data.get('weight', 0)
weight_kg = round(weight_grams / 1000, 3)
socketio.emit('scale_update', {
"grams": weight_grams,
"kilograms": weight_kg,
"timestamp": time.time()
})
return jsonify({"status": "received"}), 200

147
blueprints/sales.py Normal file
View File

@@ -0,0 +1,147 @@
from flask import Blueprint, render_template, request, jsonify, send_file, current_app
from flask_login import login_required, current_user
from core.db import get_db_connection
import io
import zipfile
from datetime import datetime
import os
sales_bp = Blueprint('sales', __name__)
@sales_bp.route('/export/db')
@login_required
def export_db():
db_file = current_app.config['DB_FILE']
if os.path.exists(db_file):
return send_file(db_file, as_attachment=True, download_name=f"SekiPOS_Backup_{datetime.now().strftime('%Y%m%d')}.db", mimetype='application/x-sqlite3')
return "Error: Database file not found", 404
@sales_bp.route('/export/images')
@login_required
def export_images():
cache_dir = current_app.config['CACHE_DIR']
if not os.path.exists(cache_dir) or not os.listdir(cache_dir):
return "No images found to export", 404
memory_file = io.BytesIO()
with zipfile.ZipFile(memory_file, 'w', zipfile.ZIP_DEFLATED) as zf:
for root, dirs, files in os.walk(cache_dir):
for file in files:
file_path = os.path.join(root, file)
zf.write(file_path, arcname=file)
memory_file.seek(0)
return send_file(
memory_file,
mimetype='application/zip',
as_attachment=True,
download_name=f"SekiPOS_Images_{datetime.now().strftime('%Y%m%d')}.zip"
)
@sales_bp.route('/sales')
@login_required
def sales():
selected_date = request.args.get('date')
payment_method = request.args.get('payment_method')
page = request.args.get('page', 1, type=int)
per_page = 100
with get_db_connection() as conn:
cur = conn.cursor()
target_date = selected_date if selected_date else cur.execute("SELECT date('now', 'localtime')").fetchone()[0]
daily_query = "SELECT SUM(total) FROM sales WHERE date(date, 'localtime') = ?"
week_query = "SELECT SUM(total) FROM sales WHERE date(date, 'localtime') >= date('now', 'localtime', '-7 days')"
month_query = "SELECT SUM(total) FROM sales WHERE strftime('%Y-%m', date, 'localtime') = strftime('%Y-%m', 'now', 'localtime')"
daily_params = [target_date]
week_params = []
month_params = []
if payment_method:
daily_query += " AND payment_method = ?"
week_query += " AND payment_method = ?"
month_query += " AND payment_method = ?"
daily_params.append(payment_method)
week_params.append(payment_method)
month_params.append(payment_method)
stats = {
"daily": cur.execute(daily_query, tuple(daily_params)).fetchone()[0] or 0,
"week": cur.execute(week_query, tuple(week_params)).fetchone()[0] or 0,
"month": cur.execute(month_query, tuple(month_params)).fetchone()[0] or 0
}
base_query = "FROM sales WHERE 1=1"
params = []
if selected_date:
base_query += " AND date(date, 'localtime') = ?"
params.append(selected_date)
if payment_method:
base_query += " AND payment_method = ?"
params.append(payment_method)
stats_query = f"SELECT COUNT(*), SUM(total) {base_query}"
count_res, sum_res = cur.execute(stats_query, tuple(params)).fetchone()
total_count = count_res or 0
total_sum = sum_res or 0
total_pages = (total_count + per_page - 1) // per_page
filtered_stats = {
"total": total_sum,
"count": total_count
}
offset = (page - 1) * per_page
data_query = f"SELECT id, date, total, payment_method {base_query} ORDER BY date DESC LIMIT ? OFFSET ?"
sales_data = cur.execute(data_query, tuple(params) + (per_page, offset)).fetchall()
return render_template('sales.html',
active_page='sales',
user=current_user,
sales=sales_data,
stats=stats,
filtered_stats=filtered_stats,
selected_date=selected_date,
selected_payment=payment_method,
current_page=page,
total_pages=total_pages)
@sales_bp.route('/api/sale/<int:sale_id>')
@login_required
def get_sale_details(sale_id):
with get_db_connection() as conn:
items = conn.execute('SELECT barcode, name, price, quantity, subtotal FROM sale_items WHERE sale_id = ?', (sale_id,)).fetchall()
item_list = [{"barcode": i[0], "name": i[1], "price": i[2], "qty": i[3], "subtotal": i[4]} for i in items]
return jsonify(item_list), 200
@sales_bp.route('/api/sale/<int:sale_id>', methods=['DELETE'])
@login_required
def reverse_sale(sale_id):
try:
with get_db_connection() as conn:
cur = conn.cursor()
items = cur.execute('SELECT barcode, quantity FROM sale_items WHERE sale_id = ?', (sale_id,)).fetchall()
for barcode, qty in items:
cur.execute('UPDATE products SET stock = stock + ? WHERE barcode = ?', (qty, barcode))
cur.execute('DELETE FROM sale_items WHERE sale_id = ?', (sale_id,))
cur.execute('DELETE FROM sales WHERE id = ?', (sale_id,))
conn.commit()
return jsonify({"status": "success"}), 200
except Exception as e:
print(f"Reverse Sale Error: {e}")
return jsonify({"error": str(e)}), 500