wersja 0.0.4 #7

Merged
gru merged 47 commits from zliczanie_wydatkow_i_poprawki_w_js into master 2025-07-28 22:17:13 +02:00
2 changed files with 99 additions and 79 deletions
Showing only changes of commit a5025b94ff - Show all commits

131
app.py
View File

@@ -116,6 +116,14 @@ failed_login_attempts = defaultdict(deque)
MAX_ATTEMPTS = 10
TIME_WINDOW = 60 * 60
WEBP_SAVE_PARAMS = {
"format": "WEBP",
"lossless": True, # lub False jeśli chcesz używać quality
"method": 6,
# "quality": 95, # tylko jeśli lossless=False
}
db = SQLAlchemy(app)
socketio = SocketIO(app, async_mode="eventlet")
login_manager = LoginManager(app)
@@ -328,8 +336,7 @@ def save_resized_image(file, path):
image.info.clear()
new_path = path.rsplit(".", 1)[0] + ".webp"
# image.save(new_path, format="WEBP", quality=100, method=0)
image.save(new_path, format="WEBP", lossless=True, method=6)
image.save(new_path, **WEBP_SAVE_PARAMS)
except Exception as e:
raise ValueError(f"Błąd podczas przetwarzania obrazu: {e}")
@@ -390,7 +397,7 @@ def rotate_receipt_by_id(receipt_id):
new_filename = generate_new_receipt_filename(receipt.list_id)
new_path = os.path.join(app.config["UPLOAD_FOLDER"], new_filename)
rotated.save(new_path, format="WEBP", quality=100)
rotated.save(new_path, **WEBP_SAVE_PARAMS)
os.remove(old_path)
receipt.filename = new_filename
@@ -486,6 +493,35 @@ def get_expenses_aggregated_by_list_created_at(
return {"labels": labels, "expenses": expenses}
def recalculate_filesizes(receipt_id: int = None):
updated = 0
not_found = 0
unchanged = 0
if receipt_id is not None:
receipt = db.session.get(Receipt, receipt_id)
receipts = [receipt] if receipt else []
else:
receipts = db.session.execute(db.select(Receipt)).scalars().all()
for r in receipts:
if not r:
continue
filepath = os.path.join(app.config["UPLOAD_FOLDER"], r.filename)
if os.path.exists(filepath):
real_size = os.path.getsize(filepath)
if r.filesize != real_size:
r.filesize = real_size
updated += 1
else:
unchanged += 1
else:
not_found += 1
db.session.commit()
return updated, unchanged, not_found
############# OCR ###########################
@@ -1431,6 +1467,7 @@ def rotate_receipt_user(receipt_id):
try:
rotate_receipt_by_id(receipt_id)
recalculate_filesizes(receipt_id)
flash("Obrócono paragon", "success")
except FileNotFoundError:
flash("Plik nie istnieje", "danger")
@@ -1730,6 +1767,15 @@ def admin_receipts(id):
try:
if id == "all":
receipts = Receipt.query.order_by(Receipt.uploaded_at.desc()).all()
# Szukaj sierot tylko dla "all"
upload_folder = app.config["UPLOAD_FOLDER"]
all_db_filenames = set(r.filename for r in receipts)
files_on_disk = set(os.listdir(upload_folder))
stale_files = [
f for f in files_on_disk
if f.endswith(".webp") and f not in all_db_filenames and f.startswith("list_")
]
else:
list_id = int(id)
receipts = (
@@ -1737,20 +1783,11 @@ def admin_receipts(id):
.order_by(Receipt.uploaded_at.desc())
.all()
)
stale_files = [] # brak sierot
except ValueError:
flash("Nieprawidłowe ID listy.", "danger")
return redirect(url_for("admin_panel"))
# Przeszukaj folder upload pod kątem „sierot”
upload_folder = app.config["UPLOAD_FOLDER"]
db_filenames = set(r.filename for r in receipts)
all_db_filenames = set(r.filename for r in Receipt.query.all()) # Wszystko z bazy
files_on_disk = set(os.listdir(upload_folder))
stale_files = [
f for f in files_on_disk
if f.endswith(".webp") and f not in all_db_filenames and f.startswith("list_")
]
# Przekaż do template: receipts (z bazy) i orphan_files (sieroty)
return render_template(
"admin/receipts.html",
receipts=receipts,
@@ -1759,34 +1796,13 @@ def admin_receipts(id):
)
@app.route("/admin/delete_orphan_receipt_file/<filename>")
@login_required
@admin_required
def delete_orphan_receipt_file(filename):
upload_folder = app.config["UPLOAD_FOLDER"]
safe_filename = os.path.basename(filename)
file_path = os.path.join(upload_folder, safe_filename)
# Dowolnego pliku nie kasujemy jeśli jest w bazie (Receipt.filename)
if Receipt.query.filter_by(filename=safe_filename).first():
flash("Nie możesz usunąć pliku powiązanego z bazą!", "danger")
return redirect(url_for("admin_receipts", id="all"))
if not os.path.exists(file_path):
flash("Plik już nie istnieje.", "warning")
else:
try:
os.remove(file_path)
flash(f"Usunięto plik: {safe_filename}", "success")
except Exception as e:
flash(f"Błąd przy usuwaniu pliku: {e}", "danger")
return redirect(url_for("admin_receipts", id="all"))
@app.route("/admin/rotate_receipt/<int:receipt_id>")
@login_required
@admin_required
def rotate_receipt(receipt_id):
try:
rotate_receipt_by_id(receipt_id)
recalculate_filesizes(receipt_id)
flash("Obrócono paragon", "success")
except FileNotFoundError:
flash("Plik nie istnieje", "danger")
@@ -1797,9 +1813,27 @@ def rotate_receipt(receipt_id):
@app.route("/admin/delete_receipt/<int:receipt_id>")
@app.route("/admin/delete_receipt/orphan/<path:filename>")
@login_required
@admin_required
def delete_receipt(receipt_id):
def delete_receipt(receipt_id=None, filename=None):
if filename: # tryb orphan
safe_filename = os.path.basename(filename)
if Receipt.query.filter_by(filename=safe_filename).first():
flash("Nie można usunąć pliku powiązanego z bazą!", "danger")
else:
file_path = os.path.join(app.config["UPLOAD_FOLDER"], safe_filename)
if os.path.exists(file_path):
try:
os.remove(file_path)
flash(f"Usunięto plik: {safe_filename}", "success")
except Exception as e:
flash(f"Błąd przy usuwaniu pliku: {e}", "danger")
else:
flash("Plik już nie istnieje.", "warning")
return redirect(url_for("admin_receipts", id="all"))
# tryb z rekordem w bazie
try:
delete_receipt_by_id(receipt_id)
flash("Paragon usunięty", "success")
@@ -1826,6 +1860,8 @@ def rename_receipt(receipt_id):
try:
os.rename(old_path, new_path)
receipt.filename = new_filename
db.session.flush()
recalculate_filesizes(receipt.id)
db.session.commit()
flash("Zmieniono nazwę pliku", "success")
except Exception as e:
@@ -2215,7 +2251,7 @@ def crop_receipt():
receipt.filename = os.path.basename(new_path)
db.session.commit()
recalculate_filesizes(receipt.id)
return jsonify(success=True)
except Exception as e:
return jsonify(success=False, error=str(e))
@@ -2224,25 +2260,8 @@ def crop_receipt():
@app.route("/admin/recalculate_filesizes")
@login_required
@admin_required
def recalculate_filesizes():
updated = 0
not_found = 0
unchanged = 0
receipts = Receipt.query.all()
for r in receipts:
filepath = os.path.join(app.config["UPLOAD_FOLDER"], r.filename)
if os.path.exists(filepath):
real_size = os.path.getsize(filepath)
if r.filesize != real_size:
r.filesize = real_size
updated += 1
else:
unchanged += 1
else:
not_found += 1
db.session.commit()
def recalculate_filesizes_all():
updated, unchanged, not_found = recalculate_filesizes()
flash(
f"Zaktualizowano: {updated}, bez zmian: {unchanged}, brak pliku: {not_found}",
"success",

View File

@@ -5,8 +5,8 @@
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
<h2 class="mb-2">📸 Wszystkie paragony</h2>
<div>
<a href="{{ url_for('recalculate_filesizes') }}" class="btn btn-sm btn-outline-primary me-2">
🔄 Przelicz rozmiary plików
<a href="{{ url_for('recalculate_filesizes_all') }}" class="btn btn-outline-primary me-2">
Przelicz rozmiary plików
</a>
<a href="/admin" class="btn btn-outline-secondary">← Powrót do panelu</a>
</div>
@@ -44,8 +44,8 @@
<a href="{{ url_for('generate_receipt_hash', receipt_id=r.id) }}"
class="btn btn-sm btn-outline-secondary w-100 mb-2">🔐 Generuj hash</a>
{% endif %}
<a href="{{ url_for('delete_receipt', receipt_id=r.id) }}"
class="btn btn-sm btn-outline-danger w-100 mb-2">🗑️
<a href="{{ url_for('delete_receipt', receipt_id=r.id) }}" class="btn btn-sm btn-outline-danger w-100 mb-2"
onclick="return confirm('Na pewno usunąć plik {{ r.filename }}?');">🗑️
Usuń</a>
<a href="{{ url_for('edit_list', list_id=r.list_id) }}" class="btn btn-sm btn-outline-light w-100 mb-2">✏️
Edytuj listę #{{ r.list_id }}</a>
@@ -64,27 +64,28 @@
</div>
</div>
{% if orphan_files %}
<hr class="my-4">
<h4 class="mt-3 mb-2 text-warning">🧐 Znalezione nieprzypisane pliki ({{ orphan_files_count }})</h4>
<div class="row g-3">
{% for f in orphan_files %}
<div class="col-6 col-md-4 col-lg-3">
<div class="card bg-dark border-warning text-warning h-100">
<img src="{{ url_for('uploaded_file', filename=f) }}" class="card-img-top" style="object-fit: cover; height: 200px;">
<div class="card-body text-center">
<p class="small mb-1 fw-bold">{{ f }}</p>
<div class="alert alert-warning small py-1 mb-2">Brak powiązania z listą!</div>
<a href="{{ url_for('delete_orphan_receipt_file', filename=f) }}"
class="btn btn-sm btn-outline-danger w-100 mb-2"
onclick="return confirm('Na pewno usunąć WYŁĄCZNIE plik {{ f }} z dysku?');">
🗑 Usuń plik z serwera
</a>
</div>
</div>
{% if orphan_files and request.path.endswith('/all') %}
<hr class="my-4">
<h4 class="mt-3 mb-2 text-warning">Znalezione nieprzypisane pliki ({{ orphan_files_count }})</h4>
<div class="row g-3">
{% for f in orphan_files %}
<div class="col-6 col-md-4 col-lg-3">
<div class="card bg-dark border-warning text-warning h-100">
<img src="{{ url_for('uploaded_file', filename=f) }}" class="card-img-top"
style="object-fit: cover; height: 200px;">
<div class="card-body text-center">
<p class="small mb-1 fw-bold">{{ f }}</p>
<div class="alert alert-warning small py-1 mb-2">Brak powiązania z listą!</div>
<a href="{{ url_for('delete_receipt', filename=f) }}" class="btn btn-sm btn-outline-danger w-100 mb-2"
onclick="return confirm('Na pewno usunąć WYŁĄCZNIE plik {{ f }} z dysku?');">
🗑 Usuń plik z serwera
</a>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{% endif %}