initial commit
this is a copy ;3
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Österreichischer Pfandrechner
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
0
PfandApplication/__init__.py
Normal file
BIN
PfandApplication/images/auszeichnung.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
PfandApplication/images/bierflasche.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
PfandApplication/images/dose.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
PfandApplication/images/flaschen.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
PfandApplication/images/joghurt glas.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
PfandApplication/images/kasten.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
PfandApplication/images/monster.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
PfandApplication/images/plastikflasche.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
1542
PfandApplication/main.py
Normal file
218
PfandApplication/pfand_scanner.py
Normal file
@@ -0,0 +1,218 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, simpledialog, messagebox
|
||||
import cv2
|
||||
from PIL import Image, ImageTk
|
||||
from pyzbar.pyzbar import decode
|
||||
from datetime import datetime, timedelta
|
||||
import threading
|
||||
import queue
|
||||
import json
|
||||
import os
|
||||
|
||||
class PfandScanner:
|
||||
def __init__(self, window, window_title):
|
||||
self.window = window
|
||||
self.window.title(window_title)
|
||||
|
||||
self.data_file = "quantities.json"
|
||||
self.load_json()
|
||||
|
||||
self.barcode_times = {}
|
||||
self.prompted_barcodes = set()
|
||||
|
||||
self.camera_frame = ttk.Frame(window)
|
||||
self.camera_frame.pack(side="left", padx=10, pady=5)
|
||||
|
||||
self.control_frame = ttk.Frame(window)
|
||||
self.control_frame.pack(side="left", padx=10, pady=5, fill="y")
|
||||
|
||||
self.info_frame = ttk.Frame(window)
|
||||
self.info_frame.pack(side="right", padx=10, pady=5, fill="both", expand=True)
|
||||
|
||||
self.camera_label = ttk.Label(self.camera_frame)
|
||||
self.camera_label.pack()
|
||||
|
||||
focus_frame = ttk.LabelFrame(self.control_frame, text="Camera Controls")
|
||||
focus_frame.pack(pady=5, padx=5, fill="x")
|
||||
|
||||
ttk.Label(focus_frame, text="Focus:").pack(pady=2)
|
||||
self.focus_slider = ttk.Scale(focus_frame, from_=0, to=255, orient="horizontal")
|
||||
self.focus_slider.set(0)
|
||||
self.focus_slider.pack(pady=2, padx=5, fill="x")
|
||||
|
||||
self.autofocus_var = tk.BooleanVar(value=True)
|
||||
self.autofocus_check = ttk.Checkbutton(
|
||||
focus_frame, text="Autofocus", variable=self.autofocus_var, command=self.toggle_autofocus)
|
||||
self.autofocus_check.pack(pady=2)
|
||||
|
||||
process_frame = ttk.LabelFrame(self.control_frame, text="Image Processing")
|
||||
process_frame.pack(pady=5, padx=5, fill="x")
|
||||
|
||||
ttk.Label(process_frame, text="Brightness:").pack(pady=2)
|
||||
self.brightness_slider = ttk.Scale(process_frame, from_=0, to=100, orient="horizontal")
|
||||
self.brightness_slider.set(50)
|
||||
self.brightness_slider.pack(pady=2, padx=5, fill="x")
|
||||
|
||||
ttk.Label(process_frame, text="Contrast:").pack(pady=2)
|
||||
self.contrast_slider = ttk.Scale(process_frame, from_=0, to=100, orient="horizontal")
|
||||
self.contrast_slider.set(50)
|
||||
self.contrast_slider.pack(pady=2, padx=5, fill="x")
|
||||
|
||||
self.tree = ttk.Treeview(self.info_frame, columns=("Time", "Barcode", "Type", "Deposit"), show="headings")
|
||||
self.tree.heading("Time", text="Time")
|
||||
self.tree.heading("Barcode", text="Barcode")
|
||||
self.tree.heading("Type", text="Type")
|
||||
self.tree.heading("Deposit", text="Deposit (€)")
|
||||
|
||||
self.tree.column("Time", width=150)
|
||||
self.tree.column("Barcode", width=200)
|
||||
self.tree.column("Type", width=100)
|
||||
self.tree.column("Deposit", width=100)
|
||||
|
||||
self.tree.pack(fill="both", expand=True)
|
||||
|
||||
scrollbar = ttk.Scrollbar(self.info_frame, orient="vertical", command=self.tree.yview)
|
||||
scrollbar.pack(side="right", fill="y")
|
||||
self.tree.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
self.cap = cv2.VideoCapture(0)
|
||||
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
|
||||
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
|
||||
self.cap.set(cv2.CAP_PROP_AUTOFOCUS, 0)
|
||||
self.cap.set(cv2.CAP_PROP_FOCUS, 0)
|
||||
|
||||
self.queue = queue.Queue()
|
||||
|
||||
self.pfand_values = {
|
||||
"EINWEG": 0.25,
|
||||
"MEHRWEG": 0.15,
|
||||
"DOSE": 0.25,
|
||||
}
|
||||
|
||||
self.process_video()
|
||||
self.window.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||||
self.process_queue()
|
||||
|
||||
def load_json(self):
|
||||
if os.path.exists(self.data_file):
|
||||
with open(self.data_file, 'r') as f:
|
||||
self.quantities = json.load(f)
|
||||
else:
|
||||
self.quantities = {}
|
||||
|
||||
def save_json(self):
|
||||
with open(self.data_file, 'w') as f:
|
||||
json.dump(self.quantities, f, indent=4)
|
||||
|
||||
def toggle_autofocus(self):
|
||||
if self.autofocus_var.get():
|
||||
self.cap.set(cv2.CAP_PROP_AUTOFOCUS, 1)
|
||||
self.focus_slider.state(['disabled'])
|
||||
else:
|
||||
self.cap.set(cv2.CAP_PROP_AUTOFOCUS, 0)
|
||||
self.focus_slider.state(['!disabled'])
|
||||
self.cap.set(cv2.CAP_PROP_FOCUS, self.focus_slider.get())
|
||||
|
||||
def adjust_image(self, frame):
|
||||
brightness = self.brightness_slider.get() / 50.0 - 1.0
|
||||
contrast = self.contrast_slider.get() / 50.0
|
||||
adjusted = cv2.convertScaleAbs(frame, alpha=contrast, beta=brightness * 127)
|
||||
gray = cv2.cvtColor(adjusted, cv2.COLOR_BGR2GRAY)
|
||||
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
|
||||
binary = cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
||||
cv2.THRESH_BINARY, 11, 2)
|
||||
return binary
|
||||
|
||||
def process_video(self):
|
||||
ret, frame = self.cap.read()
|
||||
if ret:
|
||||
if not self.autofocus_var.get():
|
||||
self.cap.set(cv2.CAP_PROP_FOCUS, self.focus_slider.get())
|
||||
|
||||
processed_frame = self.adjust_image(frame)
|
||||
barcodes = decode(processed_frame) or decode(frame)
|
||||
|
||||
for barcode in barcodes:
|
||||
points = barcode.polygon
|
||||
if len(points) == 4:
|
||||
pts = [(p.x, p.y) for p in points]
|
||||
cv2.polylines(frame, [cv2.convexHull(cv2.UMat(cv2.Mat(pts))).get()], True, (0, 255, 0), 2)
|
||||
barcode_data = barcode.data.decode('utf-8')
|
||||
self.queue.put(barcode_data)
|
||||
|
||||
cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
|
||||
img = Image.fromarray(cv2image)
|
||||
imgtk = ImageTk.PhotoImage(image=img)
|
||||
self.camera_label.imgtk = imgtk
|
||||
self.camera_label.configure(image=imgtk)
|
||||
|
||||
self.window.after(10, self.process_video)
|
||||
|
||||
def show_product_selection(self, barcode_data):
|
||||
if hasattr(self, 'product_win') and self.product_win.winfo_exists():
|
||||
return # prevent multiple dialogs
|
||||
|
||||
self.product_win = tk.Toplevel(self.window)
|
||||
self.product_win.title("Produktwahl")
|
||||
|
||||
ttk.Label(self.product_win, text=f"Welches Produkt soll dem Barcode '{barcode_data}' zugeordnet werden?").pack(pady=5)
|
||||
|
||||
selected_product = tk.StringVar()
|
||||
for prod in self.quantities:
|
||||
ttk.Radiobutton(self.product_win, text=prod, variable=selected_product, value=prod).pack(anchor='w')
|
||||
|
||||
def confirm():
|
||||
prod = selected_product.get()
|
||||
if prod:
|
||||
self.quantities[prod] += 1
|
||||
self.save_json()
|
||||
self.product_win.destroy()
|
||||
else:
|
||||
messagebox.showwarning("Keine Auswahl", "Bitte ein Produkt auswählen.")
|
||||
|
||||
ttk.Button(self.product_win, text="Bestätigen", command=confirm).pack(pady=5)
|
||||
|
||||
def process_queue(self):
|
||||
try:
|
||||
while True:
|
||||
barcode_data = self.queue.get_nowait()
|
||||
now = datetime.now()
|
||||
|
||||
if barcode_data in self.barcode_times:
|
||||
timestamps = self.barcode_times[barcode_data]
|
||||
timestamps = [t for t in timestamps if now - t <= timedelta(seconds=20)]
|
||||
if len(timestamps) >= 10:
|
||||
continue
|
||||
timestamps.append(now)
|
||||
self.barcode_times[barcode_data] = timestamps
|
||||
else:
|
||||
self.barcode_times[barcode_data] = [now]
|
||||
|
||||
current_time = now.strftime("%Y-%m-%d %H:%M:%S")
|
||||
if len(barcode_data) == 13:
|
||||
pfand_type = "EINWEG"
|
||||
elif len(barcode_data) == 8:
|
||||
pfand_type = "MEHRWEG"
|
||||
else:
|
||||
pfand_type = "DOSE"
|
||||
deposit = self.pfand_values.get(pfand_type, 0.00)
|
||||
self.tree.insert("", 0, values=(current_time, barcode_data, pfand_type, f"{deposit:.2f}"))
|
||||
|
||||
if barcode_data not in self.prompted_barcodes:
|
||||
self.prompted_barcodes.add(barcode_data)
|
||||
self.window.after(0, self.show_product_selection, barcode_data)
|
||||
|
||||
except queue.Empty:
|
||||
pass
|
||||
finally:
|
||||
self.window.after(100, self.process_queue)
|
||||
|
||||
def on_closing(self):
|
||||
if self.cap.isOpened():
|
||||
self.cap.release()
|
||||
self.window.destroy()
|
||||
|
||||
if __name__ != "__main__":
|
||||
def launch_pfand_scanner():
|
||||
scanner_window = tk.Toplevel()
|
||||
PfandScanner(scanner_window, "µScan V1.1.0")
|
||||
255
PfandApplication/updater.py
Normal file
@@ -0,0 +1,255 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
import os
|
||||
import requests
|
||||
import hashlib
|
||||
from zipfile import ZipFile
|
||||
import io
|
||||
import shutil
|
||||
import tempfile
|
||||
import traceback
|
||||
import threading
|
||||
|
||||
GITHUB_REPO_ZIP = "https://github.com/ZockerKatze/pfand/archive/refs/heads/main.zip"
|
||||
IGNORED_FILES = {"key.py"}
|
||||
|
||||
class GitHubUpdater(tk.Toplevel):
|
||||
def __init__(self, master=None):
|
||||
super().__init__(master)
|
||||
self.title("🔄 Pfand Updater")
|
||||
self.geometry("800x600")
|
||||
self.configure(bg="#ffffff")
|
||||
|
||||
self.local_dir = os.getcwd()
|
||||
self.file_differences = []
|
||||
self.structure = {}
|
||||
self.current_view = "root"
|
||||
|
||||
self._setup_style()
|
||||
self._build_ui()
|
||||
|
||||
# Run update check in background
|
||||
threading.Thread(target=self.check_for_updates, daemon=True).start()
|
||||
|
||||
def _setup_style(self):
|
||||
style = ttk.Style(self)
|
||||
style.theme_use('clam')
|
||||
|
||||
style.configure("TLabel", font=("Segoe UI", 11), background="#ffffff")
|
||||
style.configure("TButton", font=("Segoe UI", 10), padding=6, relief="flat", borderwidth=0)
|
||||
style.map("TButton", background=[("active", "#e0e0e0")])
|
||||
|
||||
style.configure("Header.TLabel", font=("Segoe UI", 20, "bold"), background="#ffffff", foreground="#333")
|
||||
style.configure("Status.TLabel", font=("Segoe UI", 12), background="#ffffff", foreground="#555")
|
||||
|
||||
style.configure("Treeview", font=("Segoe UI", 10))
|
||||
style.configure("TFrame", background="#ffffff")
|
||||
|
||||
def _build_ui(self):
|
||||
header = ttk.Label(self, text="Pfand Updater", style="Header.TLabel")
|
||||
header.pack(pady=(20, 5))
|
||||
|
||||
self.status_label = ttk.Label(self, text="🔍 Suche nach Updates...", style="Status.TLabel")
|
||||
self.status_label.pack(pady=(0, 10))
|
||||
|
||||
self.frame = ttk.Frame(self)
|
||||
self.frame.pack(expand=True, fill="both", padx=20, pady=10)
|
||||
|
||||
self.canvas = tk.Canvas(self.frame, bg="#fafafa", bd=0, highlightthickness=0)
|
||||
self.scrollbar = ttk.Scrollbar(self.frame, orient="vertical", command=self.canvas.yview)
|
||||
self.scrollable_frame = ttk.Frame(self.canvas)
|
||||
|
||||
self.scrollable_frame.bind(
|
||||
"<Configure>",
|
||||
lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))
|
||||
)
|
||||
|
||||
self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
|
||||
self.canvas.configure(yscrollcommand=self.scrollbar.set)
|
||||
|
||||
self.canvas.pack(side="left", fill="both", expand=True)
|
||||
self.scrollbar.pack(side="right", fill="y")
|
||||
|
||||
button_frame = ttk.Frame(self)
|
||||
button_frame.pack(pady=15)
|
||||
|
||||
self.back_button = ttk.Button(button_frame, text="⬅️ Zurück", command=self.show_root_view)
|
||||
self.back_button.pack(side="left", padx=10)
|
||||
self.back_button.pack_forget()
|
||||
|
||||
self.update_button = ttk.Button(button_frame, text="⬆️ Dateien aktualisieren", command=self.perform_update, state='disabled')
|
||||
self.update_button.pack(side="left", padx=10)
|
||||
|
||||
self.toggle_debug_btn = ttk.Button(self, text="🐞 Fehlerdetails anzeigen", command=self.toggle_debug_output)
|
||||
self.toggle_debug_btn.pack()
|
||||
self.toggle_debug_btn.pack_forget()
|
||||
|
||||
self.debug_output = tk.Text(self, height=8, bg="#f5f5f5", font=("Courier", 9))
|
||||
self.debug_output.pack(fill="x", padx=20, pady=(0, 10))
|
||||
self.debug_output.pack_forget()
|
||||
self.debug_visible = False
|
||||
|
||||
def toggle_debug_output(self):
|
||||
self.debug_visible = not self.debug_visible
|
||||
if self.debug_visible:
|
||||
self.debug_output.pack()
|
||||
self.toggle_debug_btn.config(text="🔽 Fehlerdetails verbergen")
|
||||
else:
|
||||
self.debug_output.pack_forget()
|
||||
self.toggle_debug_btn.config(text="🐞 Fehlerdetails anzeigen")
|
||||
|
||||
def show_root_view(self):
|
||||
self.current_view = "root"
|
||||
self.back_button.pack_forget()
|
||||
self.display_structure(self.structure)
|
||||
|
||||
def display_structure(self, struct, parent_path=""):
|
||||
for widget in self.scrollable_frame.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
for name, content in sorted(struct.items()):
|
||||
full_path = os.path.join(parent_path, name)
|
||||
lbl = ttk.Label(self.scrollable_frame, text=f"📁 {name}" if isinstance(content, dict) else f"📄 {name}", style="TLabel")
|
||||
if isinstance(content, dict):
|
||||
lbl.bind("<Double-Button-1>", lambda e, p=full_path: self.open_folder(p))
|
||||
lbl.pack(fill="x", padx=20, pady=6, anchor="w")
|
||||
|
||||
def open_folder(self, folder_path):
|
||||
self.current_view = folder_path
|
||||
self.back_button.pack()
|
||||
parts = folder_path.split(os.sep)
|
||||
subtree = self.structure
|
||||
for part in parts:
|
||||
subtree = subtree.get(part, {})
|
||||
self.display_structure(subtree, folder_path)
|
||||
|
||||
def check_for_updates(self):
|
||||
try:
|
||||
self.status_label.config(text="⬇️ Lade Update herunter...", foreground="#ffb300")
|
||||
self.update_idletasks()
|
||||
|
||||
response = requests.get(GITHUB_REPO_ZIP)
|
||||
with ZipFile(io.BytesIO(response.content)) as zip_file:
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
zip_file.extractall(temp_dir)
|
||||
extracted_path = os.path.join(temp_dir, os.listdir(temp_dir)[0])
|
||||
self.file_differences = self.compare_directories(extracted_path, self.local_dir)
|
||||
|
||||
if self.file_differences:
|
||||
self.structure = self.build_structure(self.file_differences)
|
||||
self.status_label.config(text="⚠️ Updates verfügbar", foreground="#e53935")
|
||||
self.display_structure(self.structure)
|
||||
self.update_button.config(state='normal')
|
||||
else:
|
||||
self.status_label.config(text="✅ Alles ist aktuell", foreground="#43a047")
|
||||
except Exception:
|
||||
self.status_label.config(text="❌ Fehler beim Laden", foreground="#e53935")
|
||||
self.toggle_debug_btn.pack()
|
||||
self.debug_output.insert("1.0", traceback.format_exc())
|
||||
|
||||
def compare_directories(self, src_dir, dest_dir):
|
||||
differences = []
|
||||
for root, _, files in os.walk(src_dir):
|
||||
for file in files:
|
||||
if file in IGNORED_FILES:
|
||||
continue
|
||||
src_path = os.path.join(root, file)
|
||||
rel_path = os.path.relpath(src_path, src_dir)
|
||||
dest_path = os.path.join(dest_dir, rel_path)
|
||||
|
||||
if not os.path.exists(dest_path) or not self.files_match(src_path, dest_path):
|
||||
differences.append(rel_path)
|
||||
return differences
|
||||
|
||||
def build_structure(self, file_paths):
|
||||
tree = {}
|
||||
for path in file_paths:
|
||||
parts = path.split(os.sep)
|
||||
d = tree
|
||||
for part in parts[:-1]:
|
||||
d = d.setdefault(part, {})
|
||||
d[parts[-1]] = path
|
||||
return tree
|
||||
|
||||
def files_match(self, file1, file2):
|
||||
return self.hash_file(file1) == self.hash_file(file2)
|
||||
|
||||
def hash_file(self, filepath):
|
||||
hash_md5 = hashlib.md5()
|
||||
with open(filepath, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(4096), b""):
|
||||
hash_md5.update(chunk)
|
||||
return hash_md5.hexdigest()
|
||||
|
||||
def perform_update(self):
|
||||
self.update_button.config(state='disabled')
|
||||
self.status_label.config(text="🚧 Update läuft...", foreground="#fb8c00")
|
||||
self.update_idletasks()
|
||||
|
||||
try:
|
||||
response = requests.get(GITHUB_REPO_ZIP)
|
||||
with ZipFile(io.BytesIO(response.content)) as zip_file:
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
zip_file.extractall(temp_dir)
|
||||
extracted_path = os.path.join(temp_dir, os.listdir(temp_dir)[0])
|
||||
|
||||
for rel_path in self.file_differences:
|
||||
src_path = os.path.join(extracted_path, rel_path)
|
||||
dest_path = os.path.join(self.local_dir, rel_path)
|
||||
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
||||
shutil.copy2(src_path, dest_path)
|
||||
|
||||
messagebox.showinfo("✅ Aktualisiert", "Dateien wurden erfolgreich aktualisiert.")
|
||||
self.destroy()
|
||||
except Exception as e:
|
||||
messagebox.showerror("❌ Fehler", str(e))
|
||||
self.toggle_debug_btn.pack()
|
||||
self.debug_output.insert("1.0", traceback.format_exc())
|
||||
|
||||
@staticmethod
|
||||
def files_match_static(file1, file2):
|
||||
def hash_file(filepath):
|
||||
hash_md5 = hashlib.md5()
|
||||
with open(filepath, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(4096), b""):
|
||||
hash_md5.update(chunk)
|
||||
return hash_md5.hexdigest()
|
||||
return hash_file(file1) == hash_file(file2)
|
||||
|
||||
|
||||
def run_silent_update(master=None):
|
||||
try:
|
||||
response = requests.get(GITHUB_REPO_ZIP)
|
||||
with ZipFile(io.BytesIO(response.content)) as zip_file:
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
zip_file.extractall(temp_dir)
|
||||
extracted_path = os.path.join(temp_dir, os.listdir(temp_dir)[0])
|
||||
|
||||
file_differences = []
|
||||
for root_dir, _, files in os.walk(extracted_path):
|
||||
for file in files:
|
||||
if file in IGNORED_FILES:
|
||||
continue
|
||||
src_path = os.path.join(root_dir, file)
|
||||
rel_path = os.path.relpath(src_path, extracted_path)
|
||||
dest_path = os.path.join(os.getcwd(), rel_path)
|
||||
|
||||
if not os.path.exists(dest_path) or not GitHubUpdater.files_match_static(src_path, dest_path):
|
||||
file_differences.append(rel_path)
|
||||
|
||||
if file_differences:
|
||||
result = messagebox.askyesno("🔄 Update verfügbar", "Es sind Updates verfügbar. Möchten Sie aktualisieren?")
|
||||
if result:
|
||||
updater = GitHubUpdater(master)
|
||||
updater.grab_set()
|
||||
else:
|
||||
print("Keine Updates verfügbar.")
|
||||
except Exception as e:
|
||||
print(f"Update-Check-Fehler: {e}")
|
||||
|
||||
|
||||
def open_updater():
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
updater = GitHubUpdater(root)
|
||||
updater.mainloop()
|
||||
BIN
PfandApplication/wiki/__pycache__/main.cpython-313.pyc
Normal file
19
PfandApplication/wiki/listeHOFER.csv
Normal file
@@ -0,0 +1,19 @@
|
||||
Kategorie,Produkt,Einzeln (€),Kiste inkl. aller Flaschen (€)
|
||||
Bier,"Bierflasche 0,5l",0.20,
|
||||
Bier,"Bierkiste 0,5l (LogiPack, Gösser, Stiegl, Puntigamer, Egger) 20er leer",3.00,7.00
|
||||
Alkoholfreie Getränke,Römerquelle 1l,0.29,
|
||||
Alkoholfreie Getränke,Splitkiste Römerquelle 1l 6er leer,2.00,3.74
|
||||
Alkoholfreie Getränke,Splitkiste Römerquelle 1l 12er leer,4.00,7.48
|
||||
Alkoholfreie Getränke,Coca Cola 1l,0.29,
|
||||
Alkoholfreie Getränke,Splitkiste Coca Cola 1l 6er leer,2.00,3.74
|
||||
Alkoholfreie Getränke,Splitkiste Coca Cola 1l 12er leer,4.00,7.48
|
||||
Alkoholfreie Getränke,Kiste Coca Cola 1l 12er leer,3.00,6.48
|
||||
Alkoholfreie Getränke,Normflasche 1l,0.29,
|
||||
Alkoholfreie Getränke,Normkiste 1l 6er leer,3.00,4.74
|
||||
Alkoholfreie Getränke,Exklusivmarken-Flasche 1l,0.29,
|
||||
Alkoholfreie Getränke,Exklusivmarken-Kiste 1l 6er leer,3.00,4.74
|
||||
Molkerei,Milchflasche 1l,0.22,
|
||||
Molkerei,Milchkiste 1l 6er leer,5.50,6.82
|
||||
Molkerei,Joghurtglas,0.17,
|
||||
Einweg,Kunststoffflaschen,0.25,
|
||||
Einweg,Metalldosen,0.25,
|
||||
|
27
PfandApplication/wiki/listeSPAR.csv
Normal file
@@ -0,0 +1,27 @@
|
||||
"Bierflasche Longneck 0,33 Liter","0,36"
|
||||
"Bierflasche 0,5 Liter oder 0,33 Liter","0,20"
|
||||
"Bierflasche Bügelflasche 0,5 Liter","0,36"
|
||||
"Bierflasche Bügelflasche 0,33 Liter","0,36"
|
||||
"Bierkiste 20 x 0,5 Liter","7,-"
|
||||
"Bierkiste 20 x 0,33 Liter","7,-"
|
||||
"Bierkiste 24 x 0,33 Liter","7,80"
|
||||
"Bierkiste 12 x 0,33 Liter","5,40"
|
||||
Kiste Gasteiner 6 x 1 Liter,"4,74"
|
||||
Kiste 12 x 1 Liter (AF-Getränke),"6,48"
|
||||
Kiste Vöslauer 9 x 1 Liter (PET-Flasche),"5,61"
|
||||
Kiste Vöslauer 8 x 1 Liter,"6,32"
|
||||
Kiste Vöslauer 4 x 1 Liter,"3,16"
|
||||
Getränkeflasche 1 Liter (AF),"0,29"
|
||||
Römerquelle Splitbox 6er,"3,74"
|
||||
Römerquelle Splitbox 12er,"7,48"
|
||||
"Kiste leer (20 x 0,5 Liter, 6 x 1 Liter, 12 x 1 Liter, 12 x 0,33 Liter, 24 x 0,33 Liter)","3,-"
|
||||
Kiste Bügelflasche leer 6 x 2 Liter,"3,-"
|
||||
Kiste Bügelflasche 6 x 2 Liter,"8,10"
|
||||
"Kiste Bügelflasche leer 20 x 0,5 Liter","3,-"
|
||||
"Kiste Bügelflasche 20 x 0,5 Liter","10,20"
|
||||
Bügelflasche 2 Liter,"0,85"
|
||||
Landliebe Joghurtglas 500 g,"0,17"
|
||||
"Fruchtsaftflaschen/AF-Getränke: 0,2 Liter, 0,25 Liter oder 0,33 Liter","0,14"
|
||||
Bierfass 25 Liter oder 50 Liter,"36,-"
|
||||
Milch-Glasflasche 1 Liter,"0,22"
|
||||
Original Mostflasche,"0,55"
|
||||
|
133
PfandApplication/wiki/main.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import csv
|
||||
import os
|
||||
import re
|
||||
|
||||
def select_file(callback=None):
|
||||
def set_choice(choice):
|
||||
select_window.destroy()
|
||||
if choice == "Wiki":
|
||||
open_wiki()
|
||||
else:
|
||||
filename = os.path.join("wiki", "listeSPAR.csv" if choice == "SPAR" else "listeHOFER.csv")
|
||||
if callback:
|
||||
callback(filename)
|
||||
else:
|
||||
start_app(filename)
|
||||
|
||||
select_window = tk.Tk()
|
||||
select_window.title("Wähle eine Liste")
|
||||
select_window.geometry("300x200")
|
||||
|
||||
label = tk.Label(select_window, text="Bitte eine Liste wählen:", font=("Arial", 12))
|
||||
label.pack(pady=10)
|
||||
|
||||
spar_button = tk.Button(select_window, text="SPAR", command=lambda: set_choice("SPAR"), width=15)
|
||||
spar_button.pack(pady=5)
|
||||
|
||||
hofer_button = tk.Button(select_window, text="HOFER", command=lambda: set_choice("HOFER"), width=15)
|
||||
hofer_button.pack(pady=5)
|
||||
|
||||
wiki_button = tk.Button(select_window, text="Wiki", command=lambda: set_choice("Wiki"), width=15)
|
||||
wiki_button.pack(pady=5)
|
||||
|
||||
select_window.mainloop()
|
||||
|
||||
class CSVViewerApp:
|
||||
def __init__(self, root, filename):
|
||||
self.root = root
|
||||
title = "PFANDLISTE - SPAR" if "SPAR" in filename else "PFANDLISTE - HOFER"
|
||||
self.root.title(title)
|
||||
self.root.geometry("600x400")
|
||||
|
||||
self.label = tk.Label(root, text=title, font=("Arial", 16, "bold"))
|
||||
self.label.pack(pady=10)
|
||||
|
||||
self.frame = tk.Frame(root)
|
||||
self.frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
self.tree = ttk.Treeview(self.frame)
|
||||
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
|
||||
self.scrollbar = ttk.Scrollbar(self.frame, orient="vertical", command=self.tree.yview)
|
||||
self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
self.tree.configure(yscroll=self.scrollbar.set)
|
||||
|
||||
self.load_csv(filename)
|
||||
|
||||
def load_csv(self, filename):
|
||||
try:
|
||||
with open(filename, newline='', encoding='utf-8') as file:
|
||||
reader = csv.reader(file)
|
||||
headers = next(reader, None)
|
||||
|
||||
self.tree['columns'] = headers
|
||||
self.tree.heading("#0", text="#") # First column for index
|
||||
self.tree.column("#0", width=50)
|
||||
|
||||
for header in headers:
|
||||
self.tree.heading(header, text=header)
|
||||
self.tree.column(header, anchor="center")
|
||||
|
||||
for i, row in enumerate(reader, start=1):
|
||||
self.tree.insert("", "end", text=i, values=row)
|
||||
except FileNotFoundError:
|
||||
print(f"Error: {filename} not found!")
|
||||
except Exception as e:
|
||||
print(f"Error loading CSV: {e}")
|
||||
|
||||
## Doesnt really work yet
|
||||
## In the Future maybe
|
||||
def format_markdown(text_area, text):
|
||||
text_area.tag_configure("bold", font=("Arial", 10, "bold"))
|
||||
text_area.tag_configure("center", justify="center")
|
||||
|
||||
text_area.delete("1.0", tk.END)
|
||||
|
||||
segments = []
|
||||
last_end = 0
|
||||
|
||||
for match in re.finditer(r'<p align="center">(.*?)</p>|\*\*(.*?)\*\*', text, re.DOTALL):
|
||||
segments.append((text[last_end:match.start()], None))
|
||||
|
||||
if match.group(1): # Centered text
|
||||
segments.append((match.group(1), "center"))
|
||||
elif match.group(2): # Bold text
|
||||
segments.append((match.group(2), "bold"))
|
||||
|
||||
last_end = match.end()
|
||||
|
||||
segments.append((text[last_end:], None))
|
||||
|
||||
for segment, tag in segments:
|
||||
text_area.insert(tk.END, segment, tag if tag else "")
|
||||
|
||||
def open_wiki():
|
||||
wiki_window = tk.Tk()
|
||||
wiki_window.title("Wiki")
|
||||
wiki_window.geometry("500x400")
|
||||
|
||||
text_area = tk.Text(wiki_window, wrap=tk.WORD)
|
||||
text_area.pack(expand=True, fill=tk.BOTH)
|
||||
|
||||
filename = os.path.join("wiki", "wiki.md")
|
||||
|
||||
try:
|
||||
with open(filename, "r", encoding="utf-8") as file:
|
||||
content = file.read()
|
||||
format_markdown(text_area, content)
|
||||
except FileNotFoundError:
|
||||
text_area.insert(tk.END, f"Fehler: '{filename}' nicht gefunden!")
|
||||
|
||||
wiki_window.mainloop()
|
||||
|
||||
def start_app(filename):
|
||||
root = tk.Tk()
|
||||
app = CSVViewerApp(root, filename)
|
||||
root.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
select_file()
|
||||
|
||||
3
PfandApplication/wiki/readme.md
Normal file
@@ -0,0 +1,3 @@
|
||||
big thank you to spar and hofer for the lists!
|
||||
|
||||
großes danke and spar und hofer für die listen!
|
||||
27
PfandApplication/wiki/wiki.md
Normal file
@@ -0,0 +1,27 @@
|
||||
**Pfand in Österreich**
|
||||
|
||||
Pfand bezeichnet in Österreich ein System zur Rückgabe und Wiederverwertung von Einweg- und Mehrwegverpackungen, insbesondere bei Getränkebehältern. Es dient der Reduktion von Verpackungsmüll und der Förderung nachhaltiger Kreislaufwirtschaft.
|
||||
|
||||
**Mehrwegpfand**
|
||||
|
||||
Mehrwegflaschen aus Glas oder Kunststoff werden in Österreich seit Jahrzehnten genutzt. Diese Flaschen können mehrfach wiederbefüllt werden und sind durch ein Pfandsystem in den Handel integriert. Verbraucher zahlen beim Kauf eines Produkts mit Mehrwegverpackung ein Pfand, das sie bei der Rückgabe zurückerhalten.
|
||||
|
||||
**Einwegpfand**
|
||||
|
||||
Seit dem 1. Januar 2025 gibt es in Österreich ein Pfandsystem für Einweggetränkeflaschen und -dosen. PET-Flaschen und Aluminiumdosen mit einem Volumen zwischen 0,1 und 3 Litern sind pfandpflichtig. Das Ziel dieses Systems ist es, die Recyclingquote zu erhöhen und die Umweltbelastung durch achtlos weggeworfene Verpackungen (Littering) zu verringern.
|
||||
|
||||
**Pfandhöhe und Rückgabe**
|
||||
|
||||
Die Pfandhöhe beträgt je nach Verpackungsgröße und Material zwischen 25 und 30 Cent. Die Rückgabe erfolgt in Supermärkten und anderen Verkaufsstellen, die pfandpflichtige Getränke anbieten. Dort stehen Rücknahmeautomaten zur Verfügung, die das Pfand auszahlen oder als Einkaufsgutschrift verrechnen.
|
||||
|
||||
**Umweltauswirkungen und Vorteile**
|
||||
|
||||
Das Pfandsystem trägt wesentlich zur Reduktion von Plastik- und Metallabfällen bei und steigert die Recyclingquote. Es sorgt für eine effizientere Nutzung von Ressourcen und verringert die Umweltverschmutzung. Gleichzeitig wird durch das System ein wirtschaftlicher Anreiz geschaffen, leere Verpackungen nicht achtlos wegzuwerfen.
|
||||
|
||||
**Gesetzliche Grundlagen**
|
||||
|
||||
Die Einführung des Einwegpfands basiert auf dem Abfallwirtschaftsgesetz (AWG) und entsprechenden Verordnungen der österreichischen Bundesregierung. Es orientiert sich an erfolgreichen Pfandsystemen anderer europäischer Länder wie Deutschland oder Schweden.
|
||||
|
||||
Mit dem neuen Pfandsystem macht Österreich einen wichtigen Schritt in Richtung nachhaltiger Ressourcennutzung und Kreislaufwirtschaft.
|
||||
|
||||
|
||||
74
README.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# ♻️ Pfandrechner Application Suite
|
||||
|
||||
**Version:** V.7.04.301
|
||||
**License:** [MIT](LICENSE)
|
||||
|
||||
Welcome to the **Pfandrechner Application - Package** – a sleek and powerful tool for tracking and calculating container deposits ("Pfand") in Austria 🇦🇹. Whether you're returning a few bottles or managing full bags, this app has you covered!
|
||||
|
||||
This is a copy of [Pfand](https://github.com/ZockerKatze/pfand), but this is in Package Form which can be **imported** and integrated!
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
|
||||
🔢 **Deposit Calculator** – Instantly compute the total value of your returned bottles and cans.
|
||||
|
||||
🏆 **Achievements** – Track your progress and unlock fun rewards for your deposit milestones.
|
||||
|
||||
📜 **History & Exports** – View your past returns and export the data for safekeeping or bragging rights.
|
||||
|
||||
📦 **TGTG Integration** – Check on your "Too Good To Go" orders directly within the app. ( You need to setup your API Key first! )
|
||||
|
||||
⚙️ **Smart Updater** – Keeps the app fresh with the latest features and fixes.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### 1. Clone the Repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/ZockerKatze/pfand.git
|
||||
cd pfand
|
||||
```
|
||||
|
||||
### 2. Install Dependencies
|
||||
|
||||
Make sure you’re using Python 3, then run:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 3. Launch the App
|
||||
|
||||
```bash
|
||||
python run.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧮 How to Count
|
||||
|
||||
You can either:
|
||||
|
||||
- ✍️ **Manually** count and enter your container numbers
|
||||
_OR_
|
||||
- 🔬 Use **µScan** – the improved scanner for fast and accurate counting with barcode recognition using _pyzbar_!
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Want to improve the app or add new features? Awesome!
|
||||
Fork the repo, make your changes, and send a pull request. 💡
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
||||
|
||||
---
|
||||
|
||||
Made with 💚 for recycling and a cleaner future.
|
||||
7
requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Pillow
|
||||
tgtg
|
||||
requests
|
||||
tkcalendar
|
||||
opencv-python
|
||||
pyzbar
|
||||
numpy
|
||||