402 lines
17 KiB
Python
402 lines
17 KiB
Python
import uuid as _uuid
|
|
from datetime import datetime, timezone
|
|
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
|
|
|
|
finance_bp = Blueprint('finance', __name__)
|
|
|
|
def _log_deletion(conn, entity_type, entity_uuid):
|
|
conn.execute("INSERT INTO sync_deletions (entity_type, entity_uuid) VALUES (?, ?)",
|
|
(entity_type, entity_uuid))
|
|
|
|
def _instance_id():
|
|
return current_app.config.get('INSTANCE_ID', '')
|
|
|
|
@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))
|
|
inst_id = _instance_id()
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
|
|
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,
|
|
updated_at = ?,
|
|
updated_by = ?
|
|
WHERE id = ?''', (amount, amount, now, inst_id, ticket_id))
|
|
|
|
conn.execute('''UPDATE debtor_tickets
|
|
SET status = CASE
|
|
WHEN total - amount_paid <= 0 THEN 'paid'
|
|
WHEN amount_paid > 0 THEN 'partial'
|
|
ELSE 'unpaid'
|
|
END,
|
|
updated_at = ?,
|
|
updated_by = ?
|
|
WHERE id = ?''', (now, inst_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')
|
|
inst_id = _instance_id()
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
|
|
if not ticket_id or amount <= 0:
|
|
return jsonify({"error": "Monto inválido"}), 400
|
|
|
|
try:
|
|
with get_db_connection() as conn:
|
|
conn.execute('UPDATE debtor_tickets SET amount_paid = amount_paid + ? WHERE id = ?', (amount, ticket_id))
|
|
|
|
conn.execute('''UPDATE debtor_tickets
|
|
SET status = CASE
|
|
WHEN total - amount_paid <= 0 THEN 'paid'
|
|
WHEN amount_paid > 0 THEN 'partial'
|
|
ELSE 'unpaid'
|
|
END,
|
|
updated_at = ?,
|
|
updated_by = ?
|
|
WHERE id = ?''', (now, inst_id, ticket_id))
|
|
|
|
conn.execute('''INSERT INTO sales (date, total, payment_method, uuid)
|
|
VALUES (CURRENT_TIMESTAMP, ?, ?, ?)''',
|
|
(amount, payment_method, str(_uuid.uuid4())))
|
|
|
|
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')
|
|
inst_id = _instance_id()
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
|
|
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:
|
|
existing = conn.execute("SELECT uuid FROM dicom WHERE name = ?", (name,)).fetchone()
|
|
ent_uuid = existing[0] if existing else str(_uuid.uuid4())
|
|
conn.execute('''INSERT INTO dicom (uuid, name, amount, notes, image_url, updated_at, updated_by)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
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,
|
|
updated_at = ?,
|
|
updated_by = ?''',
|
|
(ent_uuid, name, amount, notes, image_url, now, inst_id, now, inst_id))
|
|
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:
|
|
row = conn.execute("SELECT uuid FROM dicom WHERE id = ?", (debtor_id,)).fetchone()
|
|
if row:
|
|
_log_deletion(conn, 'dicom', row[0])
|
|
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()
|
|
|
|
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:
|
|
conn.execute("INSERT INTO expenses (uuid, description, amount) VALUES (?, ?, ?)",
|
|
(str(_uuid.uuid4()), 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:
|
|
row = conn.execute("SELECT uuid FROM expenses WHERE id = ?", (gasto_id,)).fetchone()
|
|
if row:
|
|
_log_deletion(conn, 'expense', row[0])
|
|
conn.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:
|
|
cur = conn.cursor()
|
|
cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='debtors'")
|
|
if not cur.fetchone():
|
|
return jsonify([])
|
|
|
|
cur.execute('SELECT id, name, contact_info, uuid FROM debtors ORDER BY name')
|
|
debtors = cur.fetchall()
|
|
|
|
return jsonify([{"id": d[0], "name": d[1], "contact_info": d[2], "uuid": d[3]} for d in debtors])
|
|
except Exception as e:
|
|
print(f"Error getting debtors: {e}")
|
|
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
|
|
inst_id = _instance_id()
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
|
|
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:
|
|
debtor_uuid = str(_uuid.uuid4())
|
|
conn.execute('''INSERT INTO debtors (uuid, name, contact_info, updated_at, updated_by)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
ON CONFLICT(name) DO UPDATE SET
|
|
contact_info = excluded.contact_info,
|
|
updated_at = excluded.updated_at,
|
|
updated_by = excluded.updated_by''',
|
|
(debtor_uuid, debtor_name, contact_info, now, inst_id))
|
|
|
|
debtor_id = conn.execute('SELECT id FROM debtors WHERE name = ?', (debtor_name,)).fetchone()[0]
|
|
|
|
status = 'partial' if initial_payment > 0 else 'unpaid'
|
|
ticket_uuid = str(_uuid.uuid4())
|
|
c = conn.execute('''INSERT INTO debtor_tickets (uuid, debtor_id, total, amount_paid, status, updated_at, updated_by)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)''',
|
|
(ticket_uuid, debtor_id, total, initial_payment, status, now, inst_id))
|
|
ticket_id = c.lastrowid
|
|
|
|
for item in cart:
|
|
conn.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')))
|
|
|
|
if item.get('barcode') and not item.get('barcode', '').startswith(('MANUAL-', 'VARIOS-', 'RAPIDA-')):
|
|
conn.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:
|
|
row = conn.execute("SELECT uuid FROM debtors WHERE id = ?", (debtor_id,)).fetchone()
|
|
if row:
|
|
_log_deletion(conn, 'debtor', row[0])
|
|
tickets = conn.execute("SELECT uuid FROM debtor_tickets WHERE debtor_id = ?", (debtor_id,)).fetchall()
|
|
for t in tickets:
|
|
_log_deletion(conn, 'ticket', t[0])
|
|
conn.execute('DELETE FROM debtor_ticket_items WHERE ticket_id IN (SELECT id FROM debtor_tickets WHERE debtor_id = ?)', (debtor_id,))
|
|
conn.execute('DELETE FROM debtor_tickets WHERE debtor_id = ?', (debtor_id,))
|
|
conn.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:
|
|
row = conn.execute("SELECT uuid FROM debtor_tickets WHERE id = ?", (ticket_id,)).fetchone()
|
|
if row:
|
|
_log_deletion(conn, 'ticket', row[0])
|
|
conn.execute('DELETE FROM debtor_ticket_items WHERE ticket_id = ?', (ticket_id,))
|
|
conn.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:
|
|
item = conn.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
|
|
conn.execute('DELETE FROM debtor_ticket_items WHERE id = ?', (item_id,))
|
|
remaining_items = conn.execute('SELECT COUNT(*) FROM debtor_ticket_items WHERE ticket_id = ?', (ticket_id,)).fetchone()[0]
|
|
|
|
if remaining_items == 0:
|
|
row = conn.execute("SELECT uuid FROM debtor_tickets WHERE id = ?", (ticket_id,)).fetchone()
|
|
if row:
|
|
_log_deletion(conn, 'ticket', row[0])
|
|
conn.execute('DELETE FROM debtor_tickets WHERE id = ?', (ticket_id,))
|
|
conn.commit()
|
|
return jsonify({"status": "success", "ticket_deleted": True}), 200
|
|
|
|
conn.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')
|
|
inst_id = _instance_id()
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
|
|
if amount <= 0:
|
|
return jsonify({"error": "Monto inválido"}), 400
|
|
|
|
with get_db_connection() as conn:
|
|
tickets = conn.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:
|
|
conn.execute('UPDATE debtor_tickets SET amount_paid = amount_paid + ?, updated_at = ?, updated_by = ? WHERE id = ?',
|
|
(remaining, now, inst_id, ticket_id))
|
|
conn.execute('UPDATE debtor_tickets SET status = \'paid\', updated_at = ?, updated_by = ? WHERE id = ?',
|
|
(now, inst_id, ticket_id))
|
|
conn.execute('''INSERT INTO sales (date, total, payment_method, uuid)
|
|
VALUES (CURRENT_TIMESTAMP, ?, ?, ?)''',
|
|
(remaining, payment_method, str(_uuid.uuid4())))
|
|
|
|
conn.commit()
|
|
|
|
return jsonify({"status": "success"}), 200
|
|
|
|
except Exception as e:
|
|
print(f"Pay All Debtor Error: {e}")
|
|
return jsonify({"error": str(e)}), 500 |