From 38616e682356f654c7032860678baad4f36028af Mon Sep 17 00:00:00 2001 From: h3r7 Date: Sat, 28 Feb 2026 08:58:10 +0100 Subject: [PATCH] v1.3 - Migration SQLite (au lieu de JSON) --- app.py | 326 ++++++++++++++++++++++++++-------------------------- config.json | 156 ++----------------------- depenses.db | Bin 0 -> 28672 bytes 3 files changed, 174 insertions(+), 308 deletions(-) create mode 100644 depenses.db diff --git a/app.py b/app.py index 86e9c6e..f835085 100644 --- a/app.py +++ b/app.py @@ -1,21 +1,43 @@ #!/usr/bin/env python3 from flask import Flask, request, jsonify, redirect, send_file -import json +import sqlite3 import os import requests import csv import io app = Flask(__name__) -CONFIG_FILE = '/home/h3r7/depenses_trello/config.json' +DB_FILE = '/home/h3r7/depenses_trello/depenses.db' -def load_config(): - with open(CONFIG_FILE, 'r') as f: - return json.load(f) +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 save_config(data): - with open(CONFIG_FILE, 'w') as f: - json.dump(data, f, indent=2) +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 "" @@ -38,66 +60,90 @@ def dashboard(): # API Config @app.route('/api/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 @app.route('/api/add', methods=['POST']) def add_depense(): - config = load_config() - data = { - 'id': len(config.get('depenses', [])) + 1, - 'prenom': request.form.get('prenom'), - 'date': parse_date(request.form.get('date', '')), - 'libelle': request.form.get('libelle'), - 'montant': float(request.form.get('montant', 0)), - 'status': 'En attente' - } - if 'depenses' not in config: - config['depenses'] = [] - config['depenses'].append(data) - save_config(config) + conn = get_db() + c = conn.cursor() + c.execute('INSERT INTO depenses (prenom, date, libelle, montant, status) VALUES (?, ?, ?, ?, ?)', + (request.form.get('prenom'), parse_date(request.form.get('date', '')), + request.form.get('libelle'), float(request.form.get('montant', 0)), 'En attente')) + conn.commit() + conn.close() return redirect('/?page=saisie') # Get depenses @app.route('/api/depenses') def get_depenses(): - config = load_config() - return jsonify(config.get('depenses', [])) + conn = get_db() + 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 @app.route('/api/del/') def delete_depense(id): - config = load_config() - config['depenses'] = [d for d in config.get('depenses', []) if d.get('id') != id] - save_config(config) + 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(): - config = load_config() - config['depenses'] = [] - save_config(config) + 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(): - config = load_config() - depenses = config.get('depenses', []) + 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 d in depenses: - writer.writerow([ - d.get('prenom', ''), - d.get('date', ''), - d.get('libelle', ''), - d.get('montant', 0), - d.get('status', '') - ]) + for row in rows: + writer.writerow(row) output.seek(0) return send_file( @@ -120,86 +166,68 @@ def import_csv(): try: content = file.read().decode('utf-8') reader = csv.reader(content.splitlines()) - next(reader) # Skip header + next(reader) - config = load_config() - if 'depenses' not in config: - config['depenses'] = [] - - max_id = max([d.get('id', 0) for d in config.get('depenses', [])], default=0) + conn = get_db() + c = conn.cursor() imported = 0 for row in reader: if len(row) >= 4: - max_id += 1 - config['depenses'].append({ - 'id': max_id, - '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' - }) + 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 - save_config(config) + conn.commit() + conn.close() return jsonify({'success': True, 'imported': imported}) except Exception as e: return jsonify({'error': str(e)}), 400 -# Generate text for ONE expense -@app.route('/api/generate_one/', 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 +# Send ONE to Trello @app.route('/api/trello/send_one/', methods=['POST']) def send_one(id): - config = load_config() - t = config.get('trello', {}) - if not t.get('api_key') or not t.get('token') or not t.get('list_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 - d = None - for exp in config.get('depenses', []): - if exp.get('id') == id: - d = exp - break + 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.get('status') == 'Envoyé ✅': + if d['status'] == 'Envoyé ✅': + conn.close() return jsonify({'error': 'Déjà envoyé'}), 400 - template = config.get('format', '{prenom} - {date} - {libelle} - {montant}€') - line = template - ddate = d.get('date', '') or '' + # 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 = 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' params = { 'key': t.get('api_key'), @@ -208,64 +236,31 @@ def send_one(id): 'name': line, 'desc': line } + try: r = requests.post(url, params=params) if r.status_code == 200: - for exp in config.get('depenses', []): - if exp.get('id') == id: - exp['status'] = 'Envoyé ✅' - save_config(config) + 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 -# 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) @app.route('/api/generate', methods=['POST']) 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 - template = config.get('format', '{prenom} - {date} - {libelle} - {montant}€') lines = [] for d in data.get('depenses', []): line = template @@ -282,42 +277,53 @@ def generate_text(): # Add prenom @app.route('/api/prenom/add', methods=['POST']) def add_prenom(): - config = load_config() + conn = get_db() + c = conn.cursor() prenom = request.form.get('prenom', '').strip() - if prenom and prenom not in config.get('prenoms', []): - if 'prenoms' not in config: - config['prenoms'] = [] - config['prenoms'].append(prenom) - save_config(config) + 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/') def del_prenom(idx): - config = load_config() - if 0 <= idx < len(config.get('prenoms', [])): - config['prenoms'].pop(idx) - save_config(config) + 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(): - config = load_config() - config['format'] = request.form.get('format', '{prenom} - {date} - {libelle} - {montant}€') - save_config(config) + 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(): - config = load_config() - config['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', '') - } - save_config(config) + }) + c.execute('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)', ('trello', t)) + conn.commit() + conn.close() return redirect('/?page=config') if __name__ == '__main__': diff --git a/config.json b/config.json index 8e0df81..f892d7e 100644 --- a/config.json +++ b/config.json @@ -1,151 +1,11 @@ { - "depenses": [ - { - "id": 1, - "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" - ], + "prenoms": ["Papa", "Maman", "H3R7"], + "format": "{prenom} - {date} - {libelle} - {montant}€", + "depenses": [], "trello": { - "api_key": "0e86fa879fe9cc1bd5bff8a4b32bceff", - "token": "ATTA55718bc108ac05e2828a9406b9a0bf843dc47e958bd659d215210a7b0726bd6f4C4A53CE", - "list_id": "698499e98171f1383a04dbd6" + "api_key": "", + "token": "", + "board_id": "", + "list_id": "" } -} \ No newline at end of file +} diff --git a/depenses.db b/depenses.db new file mode 100644 index 0000000000000000000000000000000000000000..646c52c9c3dfc4e398d3c748dee73756fdda1c29 GIT binary patch literal 28672 zcmeI5&u`mQ9Kh{3ZQ8VnH)&N=Rhu5=uvJ~b&hPAp7T0@8P3<_d9kx!BA}4WBv^43O zbTFzOI^e)*M=snDlaSE<0=Tdf5<7EXhcU4WKPAuvf(!52DeAnq_|b93zO z>7m`|Ivbl#ufIMREyiU7OBP0!w5(z@YD6cZh?GtXDHRKsv4N_FqDjUYTE%Nfs#&^H zA^T~#Vx4ri?AhxMvhXDf)hfz`8jdb3jgKd!IhN`4TFzU8Yh9B2)$Ofe-(9bdj#C#$ zmB<M!X1~|&yx~2Ep9s)v8^w3kx#1mjUvyqP*X{0>7f^IzF+83)H~YLu zJYZe)mNXNcpfjKv8c!@PvTJTQqwojLwJoRDbbPDa69M{OJVDKzBzrFG?y!->(WC4; zn#*iC8%}TF4EC0xCn$TZ|9K!hpRN_pM9U^gHys&owcBYp-R}OD^?q+?_lAf}1J&DR zFtmqT1JCSvT#{<4g^uGMvWG*bH*}7BTQxTwf^OAGGH)pwHZ4i3qP@-KnqiR1QUB+a z=t7KGJska<;Xmfh_^{%Ta z(`rpqFhLTqiY*J9#U&cz7q^0OHo zHi|;ZEy2Lr1O=Y+15;1UG^FV!rjw*}hWft}j9raTY{B2%?6PDaRYJ0=lZl5;4EkNC z(_NSdhFRkjcGef8kd&8^DyyhWx=u$zw_1y?HgSTN?bFvTqRf=i}VtU5owAA1D92LBBq$_xt$I^Ks7`UkntZtpl`E^MBlqA0ZZp;wRq{xI|R8B zOk86rvFsxX*?l1|p{in9WQZ)JNv0{4adGitl>XWZM$`O1#6M#A-^ddbFaQR?02lxR zU;qq&0WbgtzyKHk17P6AH836GjgQB~PnRzyKHk17H9Q zfB`T72EYIq00UqE3>|b?ybYsb~q)a9!<{M2>$lFaJ<0SLRyj@5O*+#(@ z8tr^K)oP}5P9c+Tw6d8(D=B7@qG0D5LN1vlP3^RtmNF?BQ*gI47}h&2iq01D=|aIN zpy`}v&=$IUw4ZT#C_a=$|^!r7)b8n~6wEe39VbaTPZ!<(N*`5(yV z{|^}c0sr{m4>0r)41fVJ00zJS7ytuc01SWuFaQR?z`-;Svk`~lBTM?Vbe1LVg5eDKjQE6Kl6XQNF9V&!2lQl17H9QfB`T72EYIq m00UqE41fXhFB7;KmgfIqw?guNZjwu|ZvG#Na5IxM|Nk36qP9r@ literal 0 HcmV?d00001