v1.3 - Migration SQLite (au lieu de JSON)

This commit is contained in:
h3r7
2026-02-28 08:58:10 +01:00
parent b40bf56c97
commit 38616e6823
3 changed files with 174 additions and 308 deletions

326
app.py
View File

@@ -1,21 +1,43 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from flask import Flask, request, jsonify, redirect, send_file from flask import Flask, request, jsonify, redirect, send_file
import json import sqlite3
import os import os
import requests import requests
import csv import csv
import io import io
app = Flask(__name__) app = Flask(__name__)
CONFIG_FILE = '/home/h3r7/depenses_trello/config.json' DB_FILE = '/home/h3r7/depenses_trello/depenses.db'
def load_config(): def init_db():
with open(CONFIG_FILE, 'r') as f: conn = sqlite3.connect(DB_FILE)
return json.load(f) 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 save_config(data): def get_db():
with open(CONFIG_FILE, 'w') as f: conn = sqlite3.connect(DB_FILE)
json.dump(data, f, indent=2) conn.row_factory = sqlite3.Row
return conn
init_db()
def parse_date(d): def parse_date(d):
if not d: return "" if not d: return ""
@@ -38,66 +60,90 @@ def dashboard():
# API Config # API Config
@app.route('/api/config') @app.route('/api/config')
def get_config(): def get_config():
return jsonify(load_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])
conn.close()
return jsonify({
'format': format_val,
'prenoms': prenoms,
'trello': trello
})
# Add depense # Add depense
@app.route('/api/add', methods=['POST']) @app.route('/api/add', methods=['POST'])
def add_depense(): def add_depense():
config = load_config() conn = get_db()
data = { c = conn.cursor()
'id': len(config.get('depenses', [])) + 1, c.execute('INSERT INTO depenses (prenom, date, libelle, montant, status) VALUES (?, ?, ?, ?, ?)',
'prenom': request.form.get('prenom'), (request.form.get('prenom'), parse_date(request.form.get('date', '')),
'date': parse_date(request.form.get('date', '')), request.form.get('libelle'), float(request.form.get('montant', 0)), 'En attente'))
'libelle': request.form.get('libelle'), conn.commit()
'montant': float(request.form.get('montant', 0)), conn.close()
'status': 'En attente'
}
if 'depenses' not in config:
config['depenses'] = []
config['depenses'].append(data)
save_config(config)
return redirect('/?page=saisie') return redirect('/?page=saisie')
# Get depenses # Get depenses
@app.route('/api/depenses') @app.route('/api/depenses')
def get_depenses(): def get_depenses():
config = load_config() conn = get_db()
return jsonify(config.get('depenses', [])) c = conn.cursor()
c.execute('SELECT id, prenom, date, libelle, montant, status FROM depenses ORDER BY date DESC')
rows = c.fetchall()
conn.close()
return jsonify([dict(row) for row in rows])
# Delete depense # Delete depense
@app.route('/api/del/<int:id>') @app.route('/api/del/<int:id>')
def delete_depense(id): def delete_depense(id):
config = load_config() conn = get_db()
config['depenses'] = [d for d in config.get('depenses', []) if d.get('id') != id] c = conn.cursor()
save_config(config) c.execute('DELETE FROM depenses WHERE id=?', (id,))
conn.commit()
conn.close()
return redirect('/?page=saisie') return redirect('/?page=saisie')
# Clear all # Clear all
@app.route('/api/clear', methods=['POST']) @app.route('/api/clear', methods=['POST'])
def clear_depenses(): def clear_depenses():
config = load_config() conn = get_db()
config['depenses'] = [] c = conn.cursor()
save_config(config) c.execute('DELETE FROM depenses')
conn.commit()
conn.close()
return jsonify({'success': True}) return jsonify({'success': True})
# Export CSV # Export CSV
@app.route('/api/export') @app.route('/api/export')
def export_csv(): def export_csv():
config = load_config() conn = get_db()
depenses = config.get('depenses', []) 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() output = io.StringIO()
writer = csv.writer(output) writer = csv.writer(output)
writer.writerow(['prenom', 'date', 'libelle', 'montant', 'status']) writer.writerow(['prenom', 'date', 'libelle', 'montant', 'status'])
for row in rows:
for d in depenses: writer.writerow(row)
writer.writerow([
d.get('prenom', ''),
d.get('date', ''),
d.get('libelle', ''),
d.get('montant', 0),
d.get('status', '')
])
output.seek(0) output.seek(0)
return send_file( return send_file(
@@ -120,86 +166,68 @@ def import_csv():
try: try:
content = file.read().decode('utf-8') content = file.read().decode('utf-8')
reader = csv.reader(content.splitlines()) reader = csv.reader(content.splitlines())
next(reader) # Skip header next(reader)
config = load_config() conn = get_db()
if 'depenses' not in config: c = conn.cursor()
config['depenses'] = []
max_id = max([d.get('id', 0) for d in config.get('depenses', [])], default=0)
imported = 0 imported = 0
for row in reader: for row in reader:
if len(row) >= 4: if len(row) >= 4:
max_id += 1 c.execute('INSERT INTO depenses (prenom, date, libelle, montant, status) VALUES (?, ?, ?, ?, ?)',
config['depenses'].append({ (row[0].strip(), row[1].strip(), row[2].strip(),
'id': max_id, float(row[3]) if row[3] else 0, row[4].strip() if len(row) > 4 else 'En attente'))
'prenom': row[0].strip(),
'date': row[1].strip(),
'libelle': row[2].strip(),
'montant': float(row[3]) if row[3] else 0,
'status': row[4].strip() if len(row) > 4 else 'En attente'
})
imported += 1 imported += 1
save_config(config) conn.commit()
conn.close()
return jsonify({'success': True, 'imported': imported}) return jsonify({'success': True, 'imported': imported})
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 400 return jsonify({'error': str(e)}), 400
# Generate text for ONE expense # Send ONE to Trello
@app.route('/api/generate_one/<int:id>', methods=['GET'])
def generate_one(id):
config = load_config()
d = None
for exp in config.get('depenses', []):
if exp.get('id') == id:
d = exp
break
if not d:
return jsonify({'text': ''})
template = config.get('format', '{prenom} - {date} - {libelle} - {montant}')
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)))
return jsonify({'text': line})
# Send to Trello - ONE card per pending expense
@app.route('/api/trello/send_one/<int:id>', methods=['POST']) @app.route('/api/trello/send_one/<int:id>', methods=['POST'])
def send_one(id): def send_one(id):
config = load_config() conn = get_db()
t = config.get('trello', {}) c = conn.cursor()
if not t.get('api_key') or not t.get('token') or not t.get('list_id'): 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 return jsonify({'error': 'Config Trello manquante'}), 400
d = None import json
for exp in config.get('depenses', []): t = json.loads(trello_row[0])
if exp.get('id') == id: if not t.get('api_key') or not t.get('token') or not t.get('list_id'):
d = exp conn.close()
break return jsonify({'error': 'Config Trello incomplète'}), 400
c.execute('SELECT * FROM depenses WHERE id=?', (id,))
d = c.fetchone()
if not d: if not d:
conn.close()
return jsonify({'error': 'Dépense non trouvée'}), 404 return jsonify({'error': 'Dépense non trouvée'}), 404
if d.get('status') == 'Envoyé ✅': if d['status'] == 'Envoyé ✅':
conn.close()
return jsonify({'error': 'Déjà envoyé'}), 400 return jsonify({'error': 'Déjà envoyé'}), 400
template = config.get('format', '{prenom} - {date} - {libelle} - {montant}') # Generate text
line = template c.execute("SELECT value FROM config WHERE key='format'")
ddate = d.get('date', '') or '' format_row = c.fetchone()
template = format_row[0] if format_row else '{prenom} - {date} - {libelle} - {montant}'
ddate = d['date'] or ''
if ddate: if ddate:
ddate = '/'.join(ddate.split('-')[::-1]) 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)))
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' url = 'https://api.trello.com/1/cards'
params = { params = {
'key': t.get('api_key'), 'key': t.get('api_key'),
@@ -208,64 +236,31 @@ def send_one(id):
'name': line, 'name': line,
'desc': line 'desc': line
} }
try: try:
r = requests.post(url, params=params) r = requests.post(url, params=params)
if r.status_code == 200: if r.status_code == 200:
for exp in config.get('depenses', []): c.execute('UPDATE depenses SET status=? WHERE id=?', ('Envoyé ✅', id))
if exp.get('id') == id: conn.commit()
exp['status'] = 'Envoyé ✅' conn.close()
save_config(config)
return jsonify({'success': True}) return jsonify({'success': True})
conn.close()
return jsonify({'error': r.text}), r.status_code return jsonify({'error': r.text}), r.status_code
except Exception as e: except Exception as e:
conn.close()
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
# Legacy - send all (for compatibility)
@app.route('/api/trello/send', methods=['POST'])
def send_trello():
config = load_config()
t = config.get('trello', {})
if not t.get('api_key') or not t.get('token') or not t.get('list_id'):
return jsonify({'error': 'Config Trello manquante'}), 400
sent_count = 0
for d in config.get('depenses', []):
if d.get('status') == 'En attente':
template = config.get('format', '{prenom} - {date} - {libelle} - {montant}')
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)))
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:
d['status'] = 'Envoyé ✅'
sent_count += 1
except:
pass
save_config(config)
return jsonify({'success': True, 'sent': sent_count})
# Generate text (all) # Generate text (all)
@app.route('/api/generate', methods=['POST']) @app.route('/api/generate', methods=['POST'])
def generate_text(): def generate_text():
config = load_config() 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 data = request.json
template = config.get('format', '{prenom} - {date} - {libelle} - {montant}')
lines = [] lines = []
for d in data.get('depenses', []): for d in data.get('depenses', []):
line = template line = template
@@ -282,42 +277,53 @@ def generate_text():
# Add prenom # Add prenom
@app.route('/api/prenom/add', methods=['POST']) @app.route('/api/prenom/add', methods=['POST'])
def add_prenom(): def add_prenom():
config = load_config() conn = get_db()
c = conn.cursor()
prenom = request.form.get('prenom', '').strip() prenom = request.form.get('prenom', '').strip()
if prenom and prenom not in config.get('prenoms', []): if prenom:
if 'prenoms' not in config: try:
config['prenoms'] = [] c.execute('INSERT INTO prenoms (name) VALUES (?)', (prenom,))
config['prenoms'].append(prenom) conn.commit()
save_config(config) except:
pass
conn.close()
return redirect('/?page=config') return redirect('/?page=config')
# Del prenom # Del prenom
@app.route('/api/prenom/del/<int:idx>') @app.route('/api/prenom/del/<int:idx>')
def del_prenom(idx): def del_prenom(idx):
config = load_config() conn = get_db()
if 0 <= idx < len(config.get('prenoms', [])): c = conn.cursor()
config['prenoms'].pop(idx) c.execute('DELETE FROM prenoms WHERE id=?', (idx+1,))
save_config(config) conn.commit()
conn.close()
return redirect('/?page=config') return redirect('/?page=config')
# Save format # Save format
@app.route('/api/format', methods=['POST']) @app.route('/api/format', methods=['POST'])
def save_format(): def save_format():
config = load_config() conn = get_db()
config['format'] = request.form.get('format', '{prenom} - {date} - {libelle} - {montant}') c = conn.cursor()
save_config(config) 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') return redirect('/?page=config')
# Save trello config # Save trello config
@app.route('/api/trello/save', methods=['POST']) @app.route('/api/trello/save', methods=['POST'])
def save_trello(): def save_trello():
config = load_config() import json
config['trello'] = { conn = get_db()
c = conn.cursor()
t = json.dumps({
'api_key': request.form.get('api_key', ''), 'api_key': request.form.get('api_key', ''),
'token': request.form.get('token', ''), 'token': request.form.get('token', ''),
'list_id': request.form.get('list_id', '') 'list_id': request.form.get('list_id', '')
} })
save_config(config) c.execute('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)', ('trello', t))
conn.commit()
conn.close()
return redirect('/?page=config') return redirect('/?page=config')
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,151 +1,11 @@
{ {
"depenses": [ "prenoms": ["Papa", "Maman", "H3R7"],
{ "format": "{prenom} - {date} - {libelle} - {montant}€",
"id": 1, "depenses": [],
"prenom": "HERY",
"date": "2026-02-06",
"libelle": "ALIEXPRESS - PISTOLET MASSAGE",
"montant": 13.67,
"status": "Envoy\u00e9 \u2705"
},
{
"id": 2,
"prenom": "HERY",
"date": "2026-02-10",
"libelle": "LUMINAIRE LAMPADAIRE",
"montant": 97.87,
"status": "Envoy\u00e9 \u2705"
},
{
"id": 3,
"prenom": "HERY",
"date": "2026-02-17",
"libelle": "ALIEXPRESS BOITIER AMPOULE REGLABLE",
"montant": 22.43,
"status": "Envoy\u00e9 \u2705"
},
{
"id": 4,
"prenom": "HERY",
"date": "2026-02-24",
"libelle": "LIDL GAUFFRIER",
"montant": 24.98,
"status": "Envoy\u00e9 \u2705"
},
{
"id": 5,
"prenom": "HERY",
"date": "2026-02-11",
"libelle": "LEETCHI FABIENNE",
"montant": 30.0,
"status": "Envoy\u00e9 \u2705"
},
{
"id": 6,
"prenom": "HERY",
"date": "2026-02-02",
"libelle": "CB LCL GAZOLE TOTAL LOOS",
"montant": 12.54,
"status": "Envoy\u00e9 \u2705"
},
{
"id": 7,
"prenom": "HERY",
"date": "2026-02-16",
"libelle": "BAR LA CLOCHE (soleil)",
"montant": 8.0,
"status": "Envoy\u00e9 \u2705"
},
{
"id": 8,
"prenom": "HERY",
"date": "2026-02-23",
"libelle": "COURSES CARREFOUR MOSELLE",
"montant": 44.47,
"status": "Envoy\u00e9 \u2705"
},
{
"id": 9,
"prenom": "HERY",
"date": "2026-02-24",
"libelle": "AMAZON CAFE FOLLIER",
"montant": 16.2,
"status": "Envoy\u00e9 \u2705"
},
{
"id": 10,
"prenom": "HERY",
"date": "2026-02-17",
"libelle": "CT CONTRE VISITE",
"montant": 25.0,
"status": "Envoy\u00e9 \u2705"
},
{
"id": 11,
"prenom": "HERY",
"date": "2026-02-16",
"libelle": "CONTROLE TECHNIQUE",
"montant": 54.0,
"status": "Envoy\u00e9 \u2705"
},
{
"id": 12,
"prenom": "HERY",
"date": "2026-02-16",
"libelle": "LOTO - EUROMILLION",
"montant": 9.0,
"status": "Envoy\u00e9 \u2705"
},
{
"id": 13,
"prenom": "HERY",
"date": "2026-02-14",
"libelle": "LUMIERE TOILETTES",
"montant": 14.78,
"status": "Envoy\u00e9 \u2705"
},
{
"id": 14,
"prenom": "HERY",
"date": "2026-02-09",
"libelle": "COMPTOIRE AFRIQ WAZEMME",
"montant": 9.4,
"status": "Envoy\u00e9 \u2705"
},
{
"id": 15,
"prenom": "HERY",
"date": "2026-02-07",
"libelle": "BOUCHERIE LILLE",
"montant": 35.02,
"status": "Envoy\u00e9 \u2705"
},
{
"id": 16,
"prenom": "HERY",
"date": "2026-02-06",
"libelle": "COURSES CARREFOUR",
"montant": 83.66,
"status": "Envoy\u00e9 \u2705"
},
{
"id": 17,
"prenom": "HERY",
"date": "2026-02-02",
"libelle": "PARKING",
"montant": 2.0,
"status": "Envoy\u00e9 \u2705"
}
],
"format": "{prenom} - {date} - {libelle} - {montant}\u20ac",
"prenoms": [
"HERY",
"Papa",
"Maman"
],
"trello": { "trello": {
"api_key": "0e86fa879fe9cc1bd5bff8a4b32bceff", "api_key": "",
"token": "ATTA55718bc108ac05e2828a9406b9a0bf843dc47e958bd659d215210a7b0726bd6f4C4A53CE", "token": "",
"list_id": "698499e98171f1383a04dbd6" "board_id": "",
"list_id": ""
} }
} }

BIN
depenses.db Normal file

Binary file not shown.