ocr #3
61
app.py
61
app.py
@@ -8,9 +8,8 @@ import platform
|
||||
import psutil
|
||||
import secrets
|
||||
import hashlib
|
||||
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
|
||||
from pillow_heif import register_heif_opener
|
||||
|
||||
@@ -228,17 +227,6 @@ def serve_css_lib(filename):
|
||||
|
||||
app.register_blueprint(static_bp)
|
||||
|
||||
|
||||
def user_has_list_access(list_obj, user):
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
if list_obj.owner_id == user.id:
|
||||
return True
|
||||
if db.session.query(SharedList).filter_by(list_id=list_obj.id, user_id=user.id).first():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def allowed_file(filename):
|
||||
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||
|
||||
@@ -376,7 +364,6 @@ def preprocess_image_for_tesseract(pil_image):
|
||||
|
||||
|
||||
def extract_total_tesseract(image):
|
||||
|
||||
text = pytesseract.image_to_string(image, lang="pol", config="--psm 6")
|
||||
lines = text.splitlines()
|
||||
candidates = []
|
||||
@@ -415,23 +402,30 @@ def extract_total_tesseract(image):
|
||||
except:
|
||||
continue
|
||||
|
||||
# Rozszerzone słowa kluczowe
|
||||
keywords = r"sum[aąo]?|razem|zapłat[ay]?|sprzedaż|opodatk|należność|do zapłaty"
|
||||
|
||||
preferred = [
|
||||
val
|
||||
for val, line in candidates
|
||||
if re.search(r"sum[aąo]?|razem|zapłaty", line.lower())
|
||||
if re.search(keywords, line.lower())
|
||||
]
|
||||
|
||||
if preferred:
|
||||
max_val = round(max(preferred), 2)
|
||||
return max_val, lines
|
||||
|
||||
# Fallback: wybierz największą wartość jeśli jest sensowna
|
||||
if candidates:
|
||||
max_val = round(max([val for val, _ in candidates]), 2)
|
||||
return max_val, lines
|
||||
# Jeśli np. większa niż 10 PLN, zakładamy że to może być suma końcowa
|
||||
if max_val >= 10:
|
||||
return max_val, lines
|
||||
|
||||
return 0.0, lines
|
||||
|
||||
|
||||
|
||||
############# END OCR #######################
|
||||
|
||||
|
||||
@@ -1050,9 +1044,6 @@ def all_products():
|
||||
@app.route("/upload_receipt/<int:list_id>", methods=["POST"])
|
||||
@login_required
|
||||
def upload_receipt(list_id):
|
||||
list_obj = db.session.get(ShoppingList, list_id)
|
||||
if not list_obj or not user_has_list_access(list_obj, current_user):
|
||||
return _receipt_error("Gość/niezalogowany nie może wgrywać plików")
|
||||
|
||||
if "receipt" not in request.files:
|
||||
return _receipt_error("Brak pliku")
|
||||
@@ -1062,8 +1053,6 @@ def upload_receipt(list_id):
|
||||
return _receipt_error("Nie wybrano pliku")
|
||||
|
||||
if file and allowed_file(file.filename):
|
||||
import hashlib
|
||||
|
||||
file_bytes = file.read()
|
||||
file.seek(0)
|
||||
file_hash = hashlib.sha256(file_bytes).hexdigest()
|
||||
@@ -1147,11 +1136,12 @@ def reorder_items():
|
||||
@app.route("/lists/<int:list_id>/analyze", methods=["POST"])
|
||||
@login_required
|
||||
def analyze_receipts_for_list(list_id):
|
||||
list_obj = db.session.get(ShoppingList, list_id)
|
||||
if not list_obj or not user_has_list_access(list_obj, current_user):
|
||||
return jsonify({"error": "Brak dostępu"}), 403
|
||||
|
||||
receipt_objs = Receipt.query.filter_by(list_id=list_id).all()
|
||||
existing_expenses = {
|
||||
e.receipt_filename for e in Expense.query.filter_by(list_id=list_id).all()
|
||||
if e.receipt_filename
|
||||
}
|
||||
|
||||
results = []
|
||||
total = 0.0
|
||||
|
||||
@@ -1171,13 +1161,18 @@ def analyze_receipts_for_list(list_id):
|
||||
value = 0.0
|
||||
lines = []
|
||||
|
||||
already_added = receipt.filename in existing_expenses
|
||||
|
||||
results.append({
|
||||
"id": receipt.id,
|
||||
"filename": receipt.filename,
|
||||
"amount": round(value, 2),
|
||||
"debug_text": lines,
|
||||
"already_added": already_added
|
||||
})
|
||||
total += value
|
||||
|
||||
if not already_added:
|
||||
total += value
|
||||
|
||||
return jsonify({"results": results, "total": round(total, 2)})
|
||||
|
||||
@@ -1483,8 +1478,6 @@ def generate_receipt_hash(receipt_id):
|
||||
flash("Plik nie istnieje", "danger")
|
||||
return redirect(request.referrer)
|
||||
|
||||
import hashlib
|
||||
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
file_hash = hashlib.sha256(f.read()).hexdigest()
|
||||
@@ -2174,8 +2167,18 @@ def handle_update_note(data):
|
||||
def handle_add_expense(data):
|
||||
list_id = data["list_id"]
|
||||
amount = data["amount"]
|
||||
receipt_filename = data.get("receipt_filename")
|
||||
|
||||
if receipt_filename:
|
||||
existing = Expense.query.filter_by(list_id=list_id, receipt_filename=receipt_filename).first()
|
||||
if existing:
|
||||
return
|
||||
new_expense = Expense(
|
||||
list_id=list_id,
|
||||
amount=amount,
|
||||
receipt_filename=receipt_filename
|
||||
)
|
||||
|
||||
new_expense = Expense(list_id=list_id, amount=amount)
|
||||
db.session.add(new_expense)
|
||||
db.session.commit()
|
||||
|
||||
|
@@ -1,31 +1,41 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.querySelectorAll('.clickable-item').forEach(item => {
|
||||
item.addEventListener('click', function (e) {
|
||||
if (!e.target.closest('button') && e.target.tagName.toLowerCase() !== 'input') {
|
||||
const checkbox = this.querySelector('input[type="checkbox"]');
|
||||
const itemsContainer = document.getElementById('items');
|
||||
if (!itemsContainer) return;
|
||||
|
||||
if (checkbox.disabled) {
|
||||
return;
|
||||
}
|
||||
itemsContainer.addEventListener('click', function (e) {
|
||||
const row = e.target.closest('.clickable-item');
|
||||
if (!row || !itemsContainer.contains(row)) return;
|
||||
|
||||
if (checkbox.checked) {
|
||||
socket.emit('uncheck_item', { item_id: parseInt(this.id.replace('item-', ''), 10) });
|
||||
} else {
|
||||
socket.emit('check_item', { item_id: parseInt(this.id.replace('item-', ''), 10) });
|
||||
}
|
||||
// Ignoruj kliknięcia w przyciski i inputy
|
||||
if (e.target.closest('button') || e.target.tagName.toLowerCase() === 'input') {
|
||||
return;
|
||||
}
|
||||
|
||||
checkbox.disabled = true;
|
||||
this.classList.add('opacity-50');
|
||||
const checkbox = row.querySelector('input[type="checkbox"]');
|
||||
if (!checkbox || checkbox.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
let existingSpinner = this.querySelector('.spinner-border');
|
||||
if (!existingSpinner) {
|
||||
const spinner = document.createElement('span');
|
||||
spinner.className = 'spinner-border spinner-border-sm ms-2';
|
||||
spinner.setAttribute('role', 'status');
|
||||
spinner.setAttribute('aria-hidden', 'true');
|
||||
checkbox.parentElement.appendChild(spinner);
|
||||
}
|
||||
}
|
||||
});
|
||||
const itemId = parseInt(row.id.replace('item-', ''), 10);
|
||||
if (isNaN(itemId)) return;
|
||||
|
||||
if (checkbox.checked) {
|
||||
socket.emit('uncheck_item', { item_id: itemId });
|
||||
} else {
|
||||
socket.emit('check_item', { item_id: itemId });
|
||||
}
|
||||
|
||||
checkbox.disabled = true;
|
||||
row.classList.add('opacity-50');
|
||||
|
||||
// Dodaj spinner tylko jeśli nie ma
|
||||
let existingSpinner = row.querySelector('.spinner-border');
|
||||
if (!existingSpinner) {
|
||||
const spinner = document.createElement('span');
|
||||
spinner.className = 'spinner-border spinner-border-sm ms-2';
|
||||
spinner.setAttribute('role', 'status');
|
||||
spinner.setAttribute('aria-hidden', 'true');
|
||||
checkbox.parentElement.appendChild(spinner);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@@ -138,12 +138,20 @@ function setupList(listId, username) {
|
||||
quantityBadge = `<span class="badge bg-secondary">x${data.quantity}</span>`;
|
||||
}
|
||||
|
||||
const countdownId = `countdown-${data.id}`;
|
||||
const countdownBtn = `
|
||||
<button type="button" class="btn btn-outline-warning" id="${countdownId}" disabled>15s</button>
|
||||
`;
|
||||
|
||||
li.innerHTML = `
|
||||
<div class="d-flex align-items-center flex-wrap gap-2 flex-grow-1">
|
||||
<input class="large-checkbox" type="checkbox">
|
||||
<span id="name-${data.id}" class="text-white">${data.name} ${quantityBadge}</span>
|
||||
<span id="name-${data.id}" class="text-white">
|
||||
${data.name} ${quantityBadge}
|
||||
</span>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
${countdownBtn}
|
||||
<button type="button" class="btn btn-outline-light"
|
||||
onclick="editItem(${data.id}, '${data.name.replace(/'/g, "\\'")}', ${data.quantity || 1})">
|
||||
✏️
|
||||
@@ -155,21 +163,33 @@ function setupList(listId, username) {
|
||||
</div>
|
||||
`;
|
||||
|
||||
// góra listy
|
||||
//document.getElementById('items').prepend(li);
|
||||
|
||||
// dół listy
|
||||
document.getElementById('items').appendChild(li);
|
||||
toggleEmptyPlaceholder();
|
||||
|
||||
// ⏳ Licznik odliczania
|
||||
let seconds = 15;
|
||||
const countdownEl = document.getElementById(countdownId);
|
||||
const intervalId = setInterval(() => {
|
||||
seconds--;
|
||||
if (countdownEl) {
|
||||
countdownEl.textContent = `${seconds}s`;
|
||||
}
|
||||
if (seconds <= 0) {
|
||||
clearInterval(intervalId);
|
||||
if (countdownEl) countdownEl.remove();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// 🔁 Request listy po 15s
|
||||
setTimeout(() => {
|
||||
if (window.LIST_ID) {
|
||||
socket.emit('request_full_list', { list_id: window.LIST_ID });
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
socket.on('item_deleted', data => {
|
||||
const li = document.getElementById(`item-${data.item_id}`);
|
||||
if (li) {
|
||||
|
@@ -7,32 +7,46 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
async function analyzeReceipts(listId) {
|
||||
const resultsDiv = document.getElementById("analysisResults");
|
||||
resultsDiv.innerHTML = `<div class="text-info">⏳ Trwa analiza paragonów...</div>`;
|
||||
resultsDiv.innerHTML = `
|
||||
<div class="text-info d-flex align-items-center gap-2">
|
||||
<div class="spinner-border spinner-border-sm text-info" role="status"></div>
|
||||
<span>Trwa analiza paragonów...</span>
|
||||
</div>`;
|
||||
|
||||
const start = performance.now(); // ⏱ START
|
||||
const start = performance.now();
|
||||
|
||||
try {
|
||||
const res = await fetch(`/lists/${listId}/analyze`, { method: "POST" });
|
||||
const data = await res.json();
|
||||
const duration = ((performance.now() - start) / 1000).toFixed(2);
|
||||
|
||||
const duration = ((performance.now() - start) / 1000).toFixed(2); // ⏱ STOP
|
||||
|
||||
let html = `<p><b>📊 Łącznie wykryto:</b> ${data.total.toFixed(2)} PLN</p>`;
|
||||
let html = `<div class="card bg-dark text-white border-secondary p-3">`;
|
||||
html += `<p><b>📊 Łącznie wykryto:</b> ${data.total.toFixed(2)} PLN</p>`;
|
||||
html += `<p class="text-secondary"><small>⏱ Czas analizy OCR: ${duration} sek.</small></p>`;
|
||||
|
||||
data.results.forEach((r, i) => {
|
||||
const disabled = r.already_added ? "disabled" : "";
|
||||
const inputStyle = "form-control d-inline-block bg-dark text-white border-light rounded";
|
||||
const inputField = `<input type="number" id="amount-${i}" value="${r.amount}" step="0.01" class="${inputStyle}" style="width: 120px;" ${disabled}>`;
|
||||
|
||||
const button = r.already_added
|
||||
? `<span class="badge bg-primary ms-2">✅ Dodano</span>`
|
||||
: `<button id="add-btn-${i}" onclick="emitExpense(${i})" class="btn btn-sm btn-outline-success ms-2">➕ Dodaj</button>`;
|
||||
|
||||
html += `
|
||||
<div class="mb-2">
|
||||
<span class="text-light">${r.filename}</span>:
|
||||
<input type="number" id="amount-${i}" value="${r.amount}" step="0.01" class="form-control d-inline-block bg-dark text-white border-light rounded" style="width: 120px;">
|
||||
<button onclick="emitExpense(${i})" class="btn btn-sm btn-outline-success ms-2">➕ Dodaj</button>
|
||||
</div>`;
|
||||
<div class="mb-2 d-flex align-items-center gap-2 flex-wrap">
|
||||
<span class="text-light flex-grow-1">${r.filename}</span>
|
||||
${inputField}
|
||||
${button}
|
||||
</div>`;
|
||||
});
|
||||
|
||||
|
||||
if (data.results.length > 1) {
|
||||
html += `<button onclick="emitAllExpenses(${data.results.length})" class="btn btn-success mt-3">➕ Dodaj wszystkie</button>`;
|
||||
html += `<button id="addAllBtn" onclick="emitAllExpenses(${data.results.length})" class="btn btn-success mt-3 w-100">➕ Dodaj wszystkie</button>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
resultsDiv.innerHTML = html;
|
||||
window._ocr_results = data.results;
|
||||
|
||||
@@ -42,22 +56,44 @@ async function analyzeReceipts(listId) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function emitExpense(i) {
|
||||
const r = window._ocr_results[i];
|
||||
const val = parseFloat(document.getElementById(`amount-${i}`).value);
|
||||
const btn = document.getElementById(`add-btn-${i}`);
|
||||
|
||||
if (!isNaN(val) && val > 0) {
|
||||
socket.emit('add_expense', {
|
||||
list_id: LIST_ID,
|
||||
amount: val
|
||||
|
||||
amount: val,
|
||||
receipt_filename: r.filename
|
||||
});
|
||||
|
||||
document.getElementById(`amount-${i}`).disabled = true;
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.classList.remove('btn-outline-success');
|
||||
btn.classList.add('btn-success');
|
||||
btn.textContent = '✅ Dodano';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function emitAllExpenses(n) {
|
||||
for (let i = 0; i < n; i++) {
|
||||
emitExpense(i);
|
||||
const btnAll = document.getElementById('addAllBtn');
|
||||
if (btnAll) {
|
||||
btnAll.disabled = true;
|
||||
btnAll.innerHTML = `<span class="spinner-border spinner-border-sm me-2" role="status"></span>Dodawanie...`;
|
||||
}
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
setTimeout(() => emitExpense(i), i * 150);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (btnAll) {
|
||||
btnAll.innerHTML = '✅ Wszystko dodano';
|
||||
btnAll.classList.remove('btn-success');
|
||||
btnAll.classList.add('btn-outline-success');
|
||||
}
|
||||
}, n * 150 + 300);
|
||||
}
|
||||
|
@@ -5,7 +5,6 @@
|
||||
<h2 class="mb-2">
|
||||
🛍️ {{ list.title }}
|
||||
|
||||
|
||||
{% if list.is_archived %}
|
||||
<span class="badge bg-secondary ms-2">(Archiwalna)</span>
|
||||
{% endif %}
|
||||
@@ -70,8 +69,6 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
|
||||
|
||||
{% else %}
|
||||
<li id="empty-placeholder" class="list-group-item bg-dark text-secondary text-center w-100">
|
||||
Brak produktów w tej liście.
|
||||
@@ -81,10 +78,12 @@
|
||||
|
||||
{% if not list.is_archived %}
|
||||
<div class="input-group mb-2">
|
||||
<input id="newItem" class="form-control bg-dark text-white border-secondary" placeholder="Dodaj produkt i ilość">
|
||||
<input id="newItem" class="form-control bg-dark text-white border-secondary" placeholder="Dodaj produkt i ilość" {% if
|
||||
not current_user.is_authenticated %}disabled{% endif %}>
|
||||
<input id="newQuantity" type="number" class="form-control bg-dark text-white border-secondary" placeholder="Ilość"
|
||||
min="1" value="1" style="max-width: 90px;">
|
||||
<button onclick="addItem({{ list.id }})" class="btn btn-success rounded-end">➕ Dodaj</button>
|
||||
min="1" value="1" style="max-width: 90px;" {% if not current_user.is_authenticated %}disabled{% endif %}>
|
||||
<button onclick="addItem({{ list.id }})" class="btn btn-success rounded-end" {% if not current_user.is_authenticated
|
||||
%}disabled{% endif %}>➕ Dodaj</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -112,9 +111,13 @@
|
||||
<h5>🧠 Analiza paragonów (OCR)</h5>
|
||||
<p class="text-small">System spróbuje automatycznie rozpoznać kwoty z dodanych paragonów.</p>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<button id="analyzeBtn" class="btn btn-outline-info mb-3">
|
||||
🔍 Zleć analizę OCR
|
||||
</button>
|
||||
{% else %}
|
||||
<div class="alert alert-warning">🔒 Tylko zalogowani użytkownicy mogą zlecać analizę OCR paragonów.</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="analysisResults" class="mt-2"></div>
|
||||
</div>
|
||||
@@ -140,7 +143,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not list.is_archived %}
|
||||
{% if not list.is_archived and current_user.is_authenticated %}
|
||||
<hr>
|
||||
<h5>📤 Dodaj zdjęcie paragonu</h5>
|
||||
<form id="receiptForm" action="{{ url_for('upload_receipt', list_id=list.id) }}" method="post"
|
||||
@@ -166,7 +169,6 @@
|
||||
|
||||
<div id="receiptGallery" class="mt-3"></div>
|
||||
</form>
|
||||
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user