412 lines
12 KiB
Python
412 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
from flask import Flask, request, jsonify, redirect, send_file
|
|
import sqlite3
|
|
import os
|
|
import requests
|
|
import csv
|
|
import io
|
|
|
|
app = Flask(__name__)
|
|
DB_FILE = '/home/h3r7/depenses_trello/depenses.db'
|
|
|
|
def init_db():
|
|
conn = sqlite3.connect(DB_FILE)
|
|
c = conn.cursor()
|
|
c.execute('''CREATE TABLE IF NOT EXISTS depenses (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
prenom TEXT,
|
|
date TEXT,
|
|
libelle TEXT,
|
|
montant REAL,
|
|
status TEXT DEFAULT 'En attente',
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)''')
|
|
c.execute('''CREATE TABLE IF NOT EXISTS config (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT
|
|
)''')
|
|
c.execute('''CREATE TABLE IF NOT EXISTS prenoms (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT UNIQUE
|
|
)''')
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
def get_db():
|
|
conn = sqlite3.connect(DB_FILE)
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
init_db()
|
|
|
|
def parse_date(d):
|
|
if not d: return ""
|
|
if "/" in d:
|
|
parts = d.split("/")
|
|
if len(parts) == 3:
|
|
return f"{parts[2]}-{parts[1]}-{parts[0]}"
|
|
return d
|
|
|
|
@app.route('/')
|
|
def index():
|
|
with open('/home/h3r7/depenses_trello/templates/index.html', 'r') as f:
|
|
return f.read()
|
|
|
|
@app.route('/dashboard')
|
|
def dashboard():
|
|
with open('/home/h3r7/depenses_trello/templates/dashboard.html', 'r') as f:
|
|
return f.read()
|
|
|
|
# API Config
|
|
@app.route('/api/config')
|
|
def get_config():
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
|
|
# Get prenoms
|
|
c.execute('SELECT name FROM prenoms')
|
|
prenoms = [row[0] for row in c.fetchall()]
|
|
|
|
# Get format
|
|
c.execute("SELECT value FROM config WHERE key='format'")
|
|
format_row = c.fetchone()
|
|
format_val = format_row[0] if format_row else '{prenom} - {date} - {libelle} - {montant}€'
|
|
|
|
# Get trello config
|
|
c.execute("SELECT value FROM config WHERE key='trello'")
|
|
trello_row = c.fetchone()
|
|
trello = {}
|
|
if trello_row:
|
|
import json
|
|
trello = json.loads(trello_row[0])
|
|
|
|
# Get categories
|
|
c.execute("SELECT value FROM config WHERE key='categories'")
|
|
cat_row = c.fetchone()
|
|
categories = cat_row[0].split(',') if cat_row else ['Courses', 'Essence', 'Loisirs', 'Maison', 'Santé', 'Transport', 'Autre']
|
|
|
|
conn.close()
|
|
|
|
return jsonify({
|
|
'format': format_val,
|
|
'prenoms': prenoms,
|
|
'categories': categories,
|
|
'trello': trello
|
|
})
|
|
|
|
# Add depense
|
|
@app.route('/api/add', methods=['POST'])
|
|
def add_depense():
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
c.execute('INSERT INTO depenses (prenom, date, libelle, montant, status, category) VALUES (?, ?, ?, ?, ?, ?)',
|
|
(request.form.get('prenom'), parse_date(request.form.get('date', '')),
|
|
request.form.get('libelle'), float(request.form.get('montant', 0)), 'En attente', request.form.get('category', 'Autre')))
|
|
conn.commit()
|
|
conn.close()
|
|
return redirect('/?page=saisie')
|
|
|
|
# Get depenses
|
|
@app.route('/api/depenses')
|
|
def get_depenses():
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
c.execute('SELECT id, prenom, date, libelle, montant, status, category FROM depenses ORDER BY date DESC')
|
|
rows = c.fetchall()
|
|
conn.close()
|
|
return jsonify([dict(row) for row in rows])
|
|
|
|
# Delete depense
|
|
@app.route('/api/del/<int:id>')
|
|
def delete_depense(id):
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
c.execute('DELETE FROM depenses WHERE id=?', (id,))
|
|
conn.commit()
|
|
conn.close()
|
|
return redirect('/?page=saisie')
|
|
|
|
# Clear all
|
|
@app.route('/api/clear', methods=['POST'])
|
|
def clear_depenses():
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
c.execute('DELETE FROM depenses')
|
|
conn.commit()
|
|
conn.close()
|
|
return jsonify({'success': True})
|
|
|
|
# Export CSV
|
|
@app.route('/api/export')
|
|
def export_csv():
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
c.execute('SELECT prenom, date, libelle, montant, status FROM depenses ORDER BY date DESC')
|
|
rows = c.fetchall()
|
|
conn.close()
|
|
|
|
output = io.StringIO()
|
|
writer = csv.writer(output)
|
|
writer.writerow(['prenom', 'date', 'libelle', 'montant', 'status'])
|
|
for row in rows:
|
|
writer.writerow(row)
|
|
|
|
output.seek(0)
|
|
return send_file(
|
|
io.BytesIO(output.getvalue().encode('utf-8')),
|
|
mimetype='text/csv',
|
|
as_attachment=True,
|
|
download_name='depenses_2026.csv'
|
|
)
|
|
|
|
# Import CSV
|
|
@app.route('/api/import', methods=['POST'])
|
|
def import_csv():
|
|
if 'file' not in request.files:
|
|
return jsonify({'error': 'Aucun fichier'}), 400
|
|
|
|
file = request.files['file']
|
|
if file.filename == '':
|
|
return jsonify({'error': 'Fichier vide'}), 400
|
|
|
|
try:
|
|
content = file.read().decode('utf-8')
|
|
reader = csv.reader(content.splitlines())
|
|
next(reader)
|
|
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
|
|
imported = 0
|
|
for row in reader:
|
|
if len(row) >= 4:
|
|
c.execute('INSERT INTO depenses (prenom, date, libelle, montant, status) VALUES (?, ?, ?, ?, ?)',
|
|
(row[0].strip(), row[1].strip(), row[2].strip(),
|
|
float(row[3]) if row[3] else 0, row[4].strip() if len(row) > 4 else 'En attente'))
|
|
imported += 1
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
return jsonify({'success': True, 'imported': imported})
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 400
|
|
|
|
# Send ONE to Trello
|
|
@app.route('/api/trello/send_one/<int:id>', methods=['POST'])
|
|
def send_one(id):
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
c.execute("SELECT value FROM config WHERE key='trello'")
|
|
trello_row = c.fetchone()
|
|
if not trello_row:
|
|
conn.close()
|
|
return jsonify({'error': 'Config Trello manquante'}), 400
|
|
|
|
import json
|
|
t = json.loads(trello_row[0])
|
|
if not t.get('api_key') or not t.get('token') or not t.get('list_id'):
|
|
conn.close()
|
|
return jsonify({'error': 'Config Trello incomplète'}), 400
|
|
|
|
c.execute('SELECT * FROM depenses WHERE id=?', (id,))
|
|
d = c.fetchone()
|
|
if not d:
|
|
conn.close()
|
|
return jsonify({'error': 'Dépense non trouvée'}), 404
|
|
|
|
if d['status'] == 'Envoyé ✅':
|
|
conn.close()
|
|
return jsonify({'error': 'Déjà envoyé'}), 400
|
|
|
|
# Generate text
|
|
c.execute("SELECT value FROM config WHERE key='format'")
|
|
format_row = c.fetchone()
|
|
template = format_row[0] if format_row else '{prenom} - {date} - {libelle} - {montant}€'
|
|
|
|
ddate = d['date'] or ''
|
|
if ddate:
|
|
ddate = '/'.join(ddate.split('-')[::-1])
|
|
|
|
line = template
|
|
line = line.replace('{prenom}', d['prenom'] or '')
|
|
line = line.replace('{date}', ddate)
|
|
line = line.replace('{libelle}', d['libelle'] or '')
|
|
line = line.replace('{montant}', str(d['montant']))
|
|
|
|
# Send to Trello
|
|
url = 'https://api.trello.com/1/cards'
|
|
params = {
|
|
'key': t.get('api_key'),
|
|
'token': t.get('token'),
|
|
'idList': t.get('list_id'),
|
|
'name': line,
|
|
'desc': line
|
|
}
|
|
|
|
try:
|
|
r = requests.post(url, params=params)
|
|
if r.status_code == 200:
|
|
c.execute('UPDATE depenses SET status=? WHERE id=?', ('Envoyé ✅', id))
|
|
conn.commit()
|
|
conn.close()
|
|
return jsonify({'success': True})
|
|
conn.close()
|
|
return jsonify({'error': r.text}), r.status_code
|
|
except Exception as e:
|
|
conn.close()
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
# Generate text (all)
|
|
@app.route('/api/generate', methods=['POST'])
|
|
def generate_text():
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
c.execute("SELECT value FROM config WHERE key='format'")
|
|
format_row = c.fetchone()
|
|
template = format_row[0] if format_row else '{prenom} - {date} - {libelle} - {montant}€'
|
|
conn.close()
|
|
|
|
data = request.json
|
|
lines = []
|
|
for d in data.get('depenses', []):
|
|
line = template
|
|
ddate = d.get('date', '') or ''
|
|
if ddate:
|
|
ddate = '/'.join(ddate.split('-')[::-1])
|
|
line = line.replace('{prenom}', d.get('prenom', ''))
|
|
line = line.replace('{date}', ddate)
|
|
line = line.replace('{libelle}', d.get('libelle', ''))
|
|
line = line.replace('{montant}', str(d.get('montant', 0)))
|
|
lines.append(line)
|
|
return jsonify({'text': '\n'.join(lines)})
|
|
|
|
# Add prenom
|
|
@app.route('/api/prenom/add', methods=['POST'])
|
|
def add_prenom():
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
prenom = request.form.get('prenom', '').strip()
|
|
if prenom:
|
|
try:
|
|
c.execute('INSERT INTO prenoms (name) VALUES (?)', (prenom,))
|
|
conn.commit()
|
|
except:
|
|
pass
|
|
conn.close()
|
|
return redirect('/?page=config')
|
|
|
|
# Del prenom
|
|
@app.route('/api/prenom/del/<int:idx>')
|
|
def del_prenom(idx):
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
c.execute('DELETE FROM prenoms WHERE id=?', (idx+1,))
|
|
conn.commit()
|
|
conn.close()
|
|
return redirect('/?page=config')
|
|
|
|
# Save format
|
|
@app.route('/api/format', methods=['POST'])
|
|
def save_format():
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
format_val = request.form.get('format', '{prenom} - {date} - {libelle} - {montant}€')
|
|
c.execute('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)', ('format', format_val))
|
|
conn.commit()
|
|
conn.close()
|
|
return redirect('/?page=config')
|
|
|
|
# Save trello config
|
|
@app.route('/api/trello/save', methods=['POST'])
|
|
def save_trello():
|
|
import json
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
t = json.dumps({
|
|
'api_key': request.form.get('api_key', ''),
|
|
'token': request.form.get('token', ''),
|
|
'list_id': request.form.get('list_id', '')
|
|
})
|
|
c.execute('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)', ('trello', t))
|
|
conn.commit()
|
|
conn.close()
|
|
return redirect('/?page=config')
|
|
|
|
|
|
|
|
# Get budget
|
|
@app.route('/api/budget')
|
|
def get_budget():
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
c.execute("SELECT value FROM config WHERE key='budget'")
|
|
row = c.fetchone()
|
|
conn.close()
|
|
return jsonify({'budget': float(row[0]) if row and row[0] else 0})
|
|
|
|
# Set budget
|
|
@app.route('/api/budget', methods=['POST'])
|
|
def set_budget():
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
budget = request.form.get('budget', 0)
|
|
c.execute('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)', ('budget', budget))
|
|
conn.commit()
|
|
conn.close()
|
|
return redirect('/?page=config')
|
|
|
|
# Get recurring
|
|
@app.route('/api/recurring')
|
|
def get_recurring():
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
c.execute('SELECT id, libelle, montant, category, jour FROM recurring')
|
|
rows = c.fetchall()
|
|
conn.close()
|
|
return jsonify([{'id': r[0], 'libelle': r[1], 'montant': r[2], 'category': r[3], 'jour': r[4]} for r in rows])
|
|
|
|
# Add recurring
|
|
@app.route('/api/recurring/add', methods=['POST'])
|
|
def add_recurring():
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
c.execute('INSERT INTO recurring (libelle, montant, category, jour) VALUES (?, ?, ?, ?)',
|
|
(request.form.get('libelle'), float(request.form.get('montant', 0)),
|
|
request.form.get('category', 'Autre'), int(request.form.get('jour', 1))))
|
|
conn.commit()
|
|
conn.close()
|
|
return redirect('/?page=config')
|
|
|
|
# Del recurring
|
|
@app.route('/api/recurring/del/<int:id>')
|
|
def del_recurring(id):
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
c.execute('DELETE FROM recurring WHERE id=?', (id,))
|
|
conn.commit()
|
|
conn.close()
|
|
return redirect('/?page=config')
|
|
|
|
# Get monthly stats
|
|
@app.route('/api/stats/monthly')
|
|
def get_monthly_stats():
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
c.execute('''SELECT strftime('%Y-%m', date) as month, SUM(montant) as total
|
|
FROM depenses GROUP BY month ORDER BY month''')
|
|
rows = c.fetchall()
|
|
conn.close()
|
|
return jsonify([{'month': r[0], 'total': r[1]} for r in rows])
|
|
|
|
# Get stats by category
|
|
@app.route('/api/stats/category')
|
|
def get_stats_category():
|
|
conn = get_db()
|
|
c = conn.cursor()
|
|
c.execute('''SELECT category, SUM(montant) as total FROM depenses GROUP BY category''')
|
|
rows = c.fetchall()
|
|
conn.close()
|
|
return jsonify([{'category': r[0] or 'Autre', 'total': r[1]} for r in rows])
|
|
|
|
if __name__ == '__main__':
|
|
app.run(host='0.0.0.0', port=8769, debug=False) |