commit ea80ed32fabb0ba2b2844d395c598fb67936f7dd Author: Rattatwinko <89251490+ZockerKatze@users.noreply.github.com> Date: Tue Apr 15 18:41:08 2025 +0200 initial commit this is a copy ;3 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7da123e --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/PfandApplication/__init__.py b/PfandApplication/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/PfandApplication/images/auszeichnung.png b/PfandApplication/images/auszeichnung.png new file mode 100644 index 0000000..0f70dfe Binary files /dev/null and b/PfandApplication/images/auszeichnung.png differ diff --git a/PfandApplication/images/bierflasche.png b/PfandApplication/images/bierflasche.png new file mode 100644 index 0000000..f97d330 Binary files /dev/null and b/PfandApplication/images/bierflasche.png differ diff --git a/PfandApplication/images/dose.png b/PfandApplication/images/dose.png new file mode 100644 index 0000000..ff773ad Binary files /dev/null and b/PfandApplication/images/dose.png differ diff --git a/PfandApplication/images/flaschen.png b/PfandApplication/images/flaschen.png new file mode 100644 index 0000000..8cb8db6 Binary files /dev/null and b/PfandApplication/images/flaschen.png differ diff --git a/PfandApplication/images/joghurt glas.png b/PfandApplication/images/joghurt glas.png new file mode 100644 index 0000000..6bc01d0 Binary files /dev/null and b/PfandApplication/images/joghurt glas.png differ diff --git a/PfandApplication/images/kasten.png b/PfandApplication/images/kasten.png new file mode 100644 index 0000000..61a0a5a Binary files /dev/null and b/PfandApplication/images/kasten.png differ diff --git a/PfandApplication/images/monster.png b/PfandApplication/images/monster.png new file mode 100644 index 0000000..beead2b Binary files /dev/null and b/PfandApplication/images/monster.png differ diff --git a/PfandApplication/images/plastikflasche.png b/PfandApplication/images/plastikflasche.png new file mode 100644 index 0000000..45315cb Binary files /dev/null and b/PfandApplication/images/plastikflasche.png differ diff --git a/PfandApplication/main.py b/PfandApplication/main.py new file mode 100644 index 0000000..06c913e --- /dev/null +++ b/PfandApplication/main.py @@ -0,0 +1,1542 @@ +import tkinter as tk +import tkinter +import webbrowser +from tkinter import ttk, messagebox, filedialog +import json + + +from PfandApplication.wiki import main as WIKI +from PfandApplication.pfand_scanner import launch_pfand_scanner +from PfandApplication.updater import open_updater as open_updater, run_silent_update + +from PIL import Image, ImageTk +import os +import subprocess +from datetime import datetime +from tkcalendar import DateEntry +import csv +import cv2 +from pyzbar.pyzbar import decode +import threading +import queue +import numpy as np +import shutil + +class Achievement: + def __init__(self, title, description, condition_type, condition_value): + self.title = title + self.description = description + self.condition_type = condition_type + self.condition_value = condition_value + self.unlocked = False + self.unlock_date = None + +class PfandCalculator: + def __init__(self, root): + self.root = root + self.root.title("Österreichischer Pfandrechner") + + # Load products and prices from JSON + self.load_products() + + self.quantities = {} + self.images = {} + self.spinboxes = {} # Store spinbox references + self.deposit_history = self.load_deposit_history() + self.scanned_barcodes = set() + self.barcode_history = [] # Store barcode scan history + + self.achievements = self.initialize_achievements() + self.load_achievements() + + if not os.path.exists('PfandApplication/images'): + os.makedirs('images') + + self.create_menu() + self.load_quantities() + self.create_widgets() + + # Scanner window + self.scanner_window = None + self.cap = None + self.scanning = False + + self.achievement_image = self.load_achievement_image() + + def load_image(self, product_name): + try: + # Use Flaschen icon for Bierflasche + if product_name == "Bierflasche": + product_name = "Flaschen" + + image_path = f"PfandApplication/images/{product_name.lower()}.png" + if os.path.exists(image_path): + try: + image = Image.open(image_path) + image = image.resize((100, 100), Image.Resampling.LANCZOS) + return ImageTk.PhotoImage(image) + except Exception as e: + print(f"Error processing image {image_path}: {e}") + return None + else: + print(f"Image not found: {image_path}") + return None + except Exception as e: + print(f"Error loading image for {product_name}: {e}") + return None + + def load_achievement_image(self): + try: + image_path = "PfandApplication/images/auszeichnung.png" + if os.path.exists(image_path): + try: + image = Image.open(image_path) + image = image.resize((50, 50), Image.Resampling.LANCZOS) + # Store both normal and gray versions + self.achievement_image = ImageTk.PhotoImage(image) + # Create grayscale version while preserving transparency + gray_image = Image.new('RGBA', image.size) + for x in range(image.width): + for y in range(image.height): + r, g, b, a = image.getpixel((x, y)) + # Convert to grayscale while preserving alpha + gray = int(0.299 * r + 0.587 * g + 0.114 * b) + # Make it lighter + gray = min(255, gray + 100) + gray_image.putpixel((x, y), (gray, gray, gray, a)) + self.achievement_image_gray = ImageTk.PhotoImage(gray_image) + return self.achievement_image + except Exception as e: + print(f"Error processing achievement image: {e}") + return None + print(f"Achievement image not found: {image_path}") + return None + except Exception as e: + print(f"Error loading achievement image: {e}") + return None + + def initialize_achievements(self): + return { + "each_100": Achievement("Krass, Weiter So!", "Du hast bis jetzt 100 von jedem Element gesammelt!", "each_element", 100), + "each_500": Achievement("Adlersson wäre neidisch!", "Adlersson wäre neidisch auf dich! Du hast 500 von jedem Element gesammelt!", "each_element", 500), + "each_1000": Achievement("Arbeitslos I", "Arbeitsamt hat angerufen! Du hast 1000 von jedem Element gesammelt!", "each_element", 1000), + "total_2000": Achievement("Arbeitslos II", "Das Arbeitsamt hat angst vor dir! Du hast 2000 totale Elemente gesammelt!", "total_elements", 2000), + "total_3000": Achievement("Arbeitslos III", "Drachenlord hat angst vor dir! Du hast mehr wie 3000 Elemente gesammelt!", "total_elements", 3000), + "total_over_3000": Achievement("Krankhafte Sucht!", "Du hast echt einen Vogel! Pfandangel #1! Du hast >3000 gesammelt!", "total_elements", 3001), + "first_deposit": Achievement("Depositer!", "Guter Anfang!", "deposits", 1), + "deposits_10": Achievement("Depositer I", "Cool, Weiter So!", "deposits", 10), + "deposits_50": Achievement("Depositer II", "WoW, Echt viele Abgaben!", "deposits", 50), + "deposits_100": Achievement("Depositer III", "Du bist der Meister der Abgaben!", "deposits", 100), + "deposits_150": Achievement("Meister Depositer", "Der Pfandautomat hat Angst vor dir, so viel wie du Abgegeben hast müsstest du eine Villa besitzen!", "deposits", 150), + # New scanner achievements + "first_scan": Achievement("Scanner Neuling", "Du hast deinen ersten Barcode gescannt!", "scans", 1), + "scans_50": Achievement("Scanner Pro", "50 Barcodes gescannt - du kennst dich aus!", "scans", 50), + "scans_100": Achievement("Scanner Meister", "100 Barcodes gescannt - der Profi ist da!", "scans", 100), + "scans_500": Achievement("Scanner Legende", "500 Barcodes gescannt - legendärer Scanner Status erreicht!", "scans", 500), + "daily_10": Achievement("Tages Champion", "10 Barcodes an einem Tag gescannt!", "daily_scans", 10), + "daily_25": Achievement("Tages Meister", "25 Barcodes an einem Tag gescannt - sehr fleißig!", "daily_scans", 25) + } + + def load_achievements(self): + try: + with open('achievements.json', 'r') as f: + data = json.load(f) + for key, achievement_data in data.items(): + if key in self.achievements: + self.achievements[key].unlocked = achievement_data['unlocked'] + self.achievements[key].unlock_date = achievement_data['unlock_date'] + except FileNotFoundError: + pass + + def save_achievements(self): + data = { + key: { + 'unlocked': achievement.unlocked, + 'unlock_date': achievement.unlock_date + } + for key, achievement in self.achievements.items() + } + with open('achievements.json', 'w') as f: + json.dump(data, f) + + def check_achievements(self): + total_elements = sum(self.deposit_history[-1]['quantities'].values()) if self.deposit_history else 0 + all_time_total = sum(sum(d['quantities'].values()) for d in self.deposit_history) + deposits_count = len(self.deposit_history) + + for achievement in ["each_100", "each_500", "each_1000"]: + if not self.achievements[achievement].unlocked: + if all(self.deposit_history[-1]['quantities'][product] >= self.achievements[achievement].condition_value + for product in self.products): + self.unlock_achievement(achievement) + + for achievement in ["total_2000", "total_3000", "total_over_3000"]: + if not self.achievements[achievement].unlocked and all_time_total >= self.achievements[achievement].condition_value: + self.unlock_achievement(achievement) + + deposit_achievements = { + 1: "first_deposit", + 10: "deposits_10", + 50: "deposits_50", + 100: "deposits_100", + 150: "deposits_150" + } + + for count, achievement_key in deposit_achievements.items(): + if not self.achievements[achievement_key].unlocked and deposits_count >= count: + self.unlock_achievement(achievement_key) + + def unlock_achievement(self, achievement_key): + achievement = self.achievements[achievement_key] + if not achievement.unlocked: + achievement.unlocked = True + achievement.unlock_date = datetime.now().strftime("%d.%m.%Y") + self.save_achievements() + messagebox.showinfo("Auszeichnung freigeschaltet!", + f"Neue Auszeichnung: {achievement.title}\n\n{achievement.description}") + + def show_achievements(self): + achievements_window = tk.Toplevel(self.root) + achievements_window.title("Auszeichnungen") + achievements_window.geometry("800x600") + + notebook = ttk.Notebook(achievements_window) + notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + all_frame = ttk.Frame(notebook) + notebook.add(all_frame, text="Alle Auszeichnungen") + + unlocked_frame = ttk.Frame(notebook) + notebook.add(unlocked_frame, text="Freigeschaltete") + + self._create_achievements_view(all_frame, show_all=True) + self._create_achievements_view(unlocked_frame, show_all=False) + + def _create_achievements_view(self, parent_frame, show_all=True): + canvas = tk.Canvas(parent_frame) + scrollbar = ttk.Scrollbar(parent_frame, orient="vertical", command=canvas.yview) + scrollable_frame = ttk.Frame(canvas) + + scrollable_frame.bind( + "", + lambda e: canvas.configure(scrollregion=canvas.bbox("all")) + ) + + canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") + canvas.configure(yscrollcommand=scrollbar.set) + + def _on_mousewheel(event): + canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") + + def _bind_mousewheel(widget): + widget.bind('', _on_mousewheel) + for child in widget.winfo_children(): + _bind_mousewheel(child) + + canvas.bind('', _on_mousewheel) + _bind_mousewheel(scrollable_frame) + + sammeln_achievements = { + "each_100": self.achievements["each_100"], + "each_500": self.achievements["each_500"], + "each_1000": self.achievements["each_1000"], + "total_2000": self.achievements["total_2000"], + "total_3000": self.achievements["total_3000"], + "total_over_3000": self.achievements["total_over_3000"] + } + + abgeben_achievements = { + "first_deposit": self.achievements["first_deposit"], + "deposits_10": self.achievements["deposits_10"], + "deposits_50": self.achievements["deposits_50"], + "deposits_100": self.achievements["deposits_100"], + "deposits_150": self.achievements["deposits_150"] + } + + scanner_achievements = { + "first_scan": self.achievements["first_scan"], + "scans_50": self.achievements["scans_50"], + "scans_100": self.achievements["scans_100"], + "scans_500": self.achievements["scans_500"], + "daily_10": self.achievements["daily_10"], + "daily_25": self.achievements["daily_25"] + } + + row = 0 + + def add_group_header(title): + nonlocal row + header_frame = ttk.Frame(scrollable_frame) + header_frame.grid(row=row, column=0, sticky='ew', padx=5, pady=(15, 5)) + + header_label = ttk.Label(header_frame, text=title, + font=('TkDefaultFont', 12, 'bold')) + header_label.pack(anchor='w') + + separator = ttk.Separator(scrollable_frame, orient='horizontal') + row += 1 + separator.grid(row=row, column=0, sticky='ew', pady=2) + row += 1 + + def add_achievement(key, achievement): + nonlocal row + if not show_all and not achievement.unlocked: + return + + frame = ttk.Frame(scrollable_frame) + frame.grid(row=row, column=0, sticky='ew', padx=5, pady=5) + + if self.achievement_image: + if achievement.unlocked: + image_label = ttk.Label(frame, image=self.achievement_image) + else: + image_label = ttk.Label(frame, image=self.achievement_image_gray) + image_label.grid(row=0, column=0, rowspan=2, padx=(5, 10), pady=5) + + content_frame = ttk.Frame(frame) + content_frame.grid(row=0, column=1, sticky='nsew', pady=5) + + title_label = ttk.Label(content_frame, text=achievement.title, + font=('TkDefaultFont', 10, 'bold')) + title_label.grid(row=0, column=0, sticky='w') + + if achievement.unlocked: + date_label = ttk.Label(content_frame, + text=f"Freigeschaltet am: {achievement.unlock_date}", + font=('TkDefaultFont', 8)) + date_label.grid(row=0, column=1, padx=(20, 0)) + + desc_label = ttk.Label(content_frame, text=achievement.description, + wraplength=600) + desc_label.grid(row=1, column=0, columnspan=2, sticky='w', pady=(2, 0)) + + content_frame.grid_columnconfigure(0, weight=1) + content_frame.grid_columnconfigure(1, weight=0) + + separator = ttk.Separator(scrollable_frame, orient='horizontal') + row += 1 + separator.grid(row=row, column=0, sticky='ew', pady=5) + row += 1 + + _bind_mousewheel(frame) + + add_group_header("Sammeln") + for key, achievement in sammeln_achievements.items(): + add_achievement(key, achievement) + + add_group_header("Abgeben") + for key, achievement in abgeben_achievements.items(): + add_achievement(key, achievement) + + add_group_header("Scanner") + for key, achievement in scanner_achievements.items(): + add_achievement(key, achievement) + + canvas.pack(side="left", fill="both", expand=True) + scrollbar.pack(side="right", fill="y") + + # Credit Section (Overall made nicer in Version 7.04.001) || Changed up a bit in V7.04.101 + + def create_credits(self): + about_window = tk.Toplevel(self.root) + about_window.title("Über Programm") + about_window.resizable(True, True) + + # Configure grid weights so widgets expand properly | This is some new Stuff! + about_window.grid_columnconfigure(0, weight=1) + about_window.grid_columnconfigure(1, weight=1) + about_window.grid_rowconfigure(0, weight=1) + + label = tk.Label(about_window, + text=( + "PfandApp V.7.04.301\n" + "Erstellt mit TKinter, CV2, Numpy, PyZbar, TGTG-API, TKCalendar, Datetime\n" + "Eigene Module: Updater, TGTG_OC, Wiki, BuildUtil\n" + "Großen Dank an SPAR, HOFER\n" + "Daten werden in RootFS gespeichert\n" + "Danke für die Idee --> Österreich" + ), + padx=10, + pady=10, + justify="center", + anchor="center") + label.grid(row=0, column=0, columnspan=2, pady=10, sticky="nsew") + + url = "https://zockerkatze.github.io/ZockerKatze/" + + # Website button + website_button = tk.Button(about_window, text="WebSite", command=lambda: webbrowser.open(url)) + website_button.grid(row=1, column=0, padx=10, pady=10, sticky="ew") + + # Close button + close_button = tk.Button(about_window, text="Close", command=about_window.destroy) + close_button.grid(row=1, column=1, padx=10, pady=10, sticky="ew") + + def update_credits(self): # Credits for the Updater Application (not some update function for some credits) || Rewrote this in Version 7.04.101 => Inconsistency is key + about_update = tk.Toplevel(self.root) + about_update.title("Über UpdaterApp") + about_update.geometry("650x190") + + about_update.grid_columnconfigure(0, weight=1) # horizont + about_update.grid_rowconfigure(0, weight=1) # vertically + about_update.grid_rowconfigure(1, weight=0) # tight :3 (OwO) + + # Text content + label_update_app = tk.Label( + about_update, + text=( + "Updater App für PfandApp\n" + "Version 1.200.000\n" + "Diese Updater App nutzt das GitHub Repository, um die App zu updaten.\n" + "Nach Updates sollte die App neugestartet (oder reloaded, bei UI) werden.\n" + "Beim Starten der App wird nach Updates gesucht!" + ), + justify="left", anchor="center" + ) + label_update_app.grid(row=0, column=0, sticky='nsew', padx=10, pady=10) + + # Close button at the bottom (like u) :3 + close_button = tk.Button(about_update, text="Close", command=about_update.destroy) + close_button.grid(row=1, column=0, sticky='ew', padx=10, pady=(0, 10)) + + def µScan_credits(self): + about_µScan = tk.Toplevel(self.root) + about_μScan.title("Über µScan") + about_μScan.geometry("650x190") + + about_μScan.grid_columnconfigure(0, weight=1) + about_μScan.grid_rowconfigure(0, weight=1) + about_μScan.grid_rowconfigure(1, weight=0) + + label_µScan_app = tk.Label( + about_μScan, + text=( + "µScan - Der bessere Barcode Scanner\n" + "Version 1.1.0\n" + "µScan erfordert einen UI Reload (Strg+R) in der Root Anwendung\n" + "µScan ist für mehrere Barcodes gemacht, die in einer kurzen Zeit gescannt werden sollten\n" + ), + justify="left", anchor="center" + ) + label_μScan_app.grid(row=0, column=0, sticky="nsew", padx=10, pady=10) + + close_button = tk.Button(about_µScan, text="Close", command=about_μScan.destroy) + close_button.grid(row=1, column=0, sticky='ew', padx=10, pady=(0, 10)) + + def create_menu(self): + self.menubar = tk.Menu(self.root) + self.root.config(menu=self.menubar) + + # "Datei" Menu + + file_menu = tk.Menu(self.menubar, tearoff=0) + self.menubar.add_cascade(label="Datei", menu=file_menu) + file_menu.add_command(label="Speichern", command=self.save_quantities, accelerator="Strg+S") + file_menu.add_command(label="Ordner öffnen", command=self.open_file_location, accelerator="Strg+O") + file_menu.add_command(label="Speicherdatei löschen", command=self.remove_save_file, accelerator="Strg+Shift+F1") + file_menu.add_separator() + file_menu.add_command(label="Neulanden der UI", command=self.recreate_widgets, accelerator="Strg+R") + file_menu.add_command(label="Updater", command=open_updater, accelerator="Strg+U") # Added this to the File Menu too! + file_menu.add_separator() + file_menu.add_command(label="Öffne PfandListe", command=WIKI.select_file, accelerator="Strg+L") + file_menu.add_command(label="Beenden", command=self.root.quit, accelerator="Strg+Q") + file_menu.add_separator() + file_menu.add_command(label="Über Programm", command=self.create_credits, accelerator="Strg+F10") + + # Deposit Menu + + deposit_menu = tk.Menu(self.menubar, tearoff=0) + self.menubar.add_cascade(label="Pfand", menu=deposit_menu) + deposit_menu.add_command(label="Pfand Abgeben", command=self.quick_deposit, accelerator="Strg+D") + deposit_menu.add_command(label="Abgabe Historie", command=self.show_deposit_history, accelerator="Strg+H") + deposit_menu.add_separator() + deposit_menu.add_command(label="Historie Exportieren (CSV)", command=self.export_history_csv, accelerator="Strg+E") + deposit_menu.add_command(label="Historie Löschen", command=self.clear_deposit_history, accelerator="Strg+Shift+F2") + + # Scanner Menu + + scanner_menu = tk.Menu(self.menubar, tearoff=0) + self.menubar.add_cascade(label="Scanner", menu=scanner_menu) + scanner_menu.add_command(label="Scanner öffnen", command=self.open_scanner_window, accelerator="Strg+B") + scanner_menu.add_separator() + scanner_menu.add_command(label="Öffne µScan", command=launch_pfand_scanner, accelerator="Strg+Shift+B") #µScan + scanner_menu.add_command(label="Über µScan", command=self.µScan_credits) #µScan credits + scanner_menu.add_separator() + scanner_menu.add_command(label="Barcodes Exportieren (CSV)", command=self.export_barcodes_csv, accelerator="Strg+Shift+E") + + # Achivements Menu + + achievements_menu = tk.Menu(self.menubar, tearoff=0) + self.menubar.add_cascade(label="Auszeichnungen", menu=achievements_menu) + achievements_menu.add_command(label="Auszeichnungen anzeigen", command=self.show_achievements, accelerator="Strg+F6") + achievements_menu.add_command(label="Auszeichnungen löschen", command=self.delete_achievements, accelerator="Strg+F7") + + # Add custom products menu + + products_menu = tk.Menu(self.menubar, tearoff=0) + self.menubar.add_cascade(label="Produkte", menu=products_menu) + products_menu.add_command(label="Produkt hinzufügen", command=self.show_add_product_window, accelerator="Strg+P") + products_menu.add_command(label="Produkte verwalten", command=self.show_manage_products_window, accelerator="Strg+Shift+P") + + + update_menu = tk.Menu(self.menubar, tearoff=0) + self.menubar.add_cascade(label="Updater", menu=update_menu) + update_menu.add_command(label="Öffne Updater", command=open_updater, accelerator="Strg+U") # Version (7.4.000 Updater Version) + update_menu.add_separator() + update_menu.add_command(label="Über Updater", command=self.update_credits) # Also no keybind here, same reason as the tgtg one #V7.4.001 + + # Manage Keybinds + + self.root.bind('', lambda e: self.save_quantities()) + self.root.bind('', lambda e: self.open_file_location()) + self.root.bind('', lambda e: self.root.quit()) + self.root.bind('', lambda e: self.quick_deposit()) + self.root.bind('', lambda e: self.show_deposit_history()) + self.root.bind('', lambda e: self.export_history_csv()) + self.root.bind('', lambda e: self.open_scanner_window()) + self.root.bind('', lambda e: launch_pfand_scanner()) #µScan + self.root.bind('', lambda e: self.handle_shift_f1(e)) + self.root.bind('', lambda e: self.handle_shift_f2(e)) + self.root.bind('', lambda e: self.show_achievements()) + self.root.bind('', lambda e: self.delete_achievements()) + self.root.bind('', lambda e: start_tgtg(self.root)) + self.root.bind('', lambda e: ask_for_tokens()) + self.root.bind('', lambda e: self.create_credits()) + self.root.bind('', lambda e: self.export_barcodes_csv() if e.state & 0x1 else self.export_history_csv()) + self.root.bind('', lambda e: self.show_add_product_window()) + self.root.bind('', lambda e: self.show_manage_products_window() if e.state & 0x1 else self.show_add_product_window()) + self.root.bind('', lambda e: WIKI.select_file()) + self.root.bind('', lambda e: self.recreate_widgets()) + self.root.bind('', lambda e: open_updater()) # New Update Feature (Version 7.4.000 UPDATER) + + def open_file_location(self): + current_dir = os.getcwd() + if os.name == 'nt': # Windows + os.startfile(current_dir) + else: # Linux/Mac + try: + subprocess.run(['xdg-open', current_dir]) + except: + subprocess.run(['open', current_dir]) + + def remove_save_file(self): + if os.path.exists('quantities.json'): + if messagebox.askyesno("Löschen bestätigen", "Sind Sie sicher, dass Sie die Speicherdatei löschen möchten?"): + try: + os.remove('quantities.json') + messagebox.showinfo("Erfolg", "Speicherdatei wurde erfolgreich gelöscht!") + self.quantities = {product: 0 for product in self.products} + self.update_total() + for widget in self.root.winfo_children(): + if isinstance(widget, ttk.Frame): + for child in widget.winfo_children(): + if isinstance(child, ttk.Frame): + for grandchild in child.winfo_children(): + if isinstance(grandchild, ttk.Spinbox): + grandchild.set("0") + except Exception as e: + messagebox.showerror("Fehler", f"Datei konnte nicht gelöscht werden: {str(e)}") + else: + messagebox.showinfo("Info", "Keine Speicherdatei vorhanden.") + + def load_quantities(self): + try: + with open('quantities.json', 'r') as f: + self.quantities = json.load(f) + except FileNotFoundError: + self.quantities = {product: 0 for product in self.products} + + def save_quantities(self): + try: + with open('quantities.json', 'w') as f: + json.dump(self.quantities, f) + messagebox.showinfo("Erfolg", "Mengen wurden erfolgreich gespeichert!") + except Exception as e: + messagebox.showerror("Fehler", f"Fehler beim Speichern der Mengen: {str(e)}") + + def create_widgets(self): + main_frame = ttk.Frame(self.root) + main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + for i, product in enumerate(self.products): + frame = ttk.Frame(main_frame) + frame.grid(row=0, column=i, padx=10, pady=5) + + image = self.load_image(product) + if image: + self.images[product] = image + label = ttk.Label(frame, image=self.images[product]) + label.grid(row=0, column=0, pady=5) + else: + canvas = tk.Canvas(frame, width=100, height=100, bg='white') + canvas.grid(row=0, column=0, pady=5) + canvas.create_text(50, 50, text=f"Kein {product}\nBild gefunden") + + ttk.Label(frame, text=product).grid(row=1, column=0, pady=2) + ttk.Label(frame, text=f"€{self.PRICES[product]:.2f}").grid(row=2, column=0, pady=2) + + quantity_var = tk.StringVar(value=str(self.quantities.get(product, 0))) + spinbox = ttk.Spinbox( + frame, + from_=0, + to=100, + width=5, + textvariable=quantity_var, + command=lambda p=product, v=quantity_var: self.update_quantity(p, v) + ) + spinbox.grid(row=3, column=0, pady=5) + + # Store spinbox reference + self.spinboxes[product] = spinbox + + spinbox.bind('', lambda event, p=product, v=quantity_var: self.update_quantity(p, v)) + spinbox.bind('', lambda event, p=product, v=quantity_var: self.update_quantity(p, v)) + + self.total_label = ttk.Label(main_frame, text="Gesamt: €0.00", font=('TkDefaultFont', 10, 'bold')) + self.total_label.grid(row=1, column=0, columnspan=len(self.products), pady=10) + + self.update_total() + + def update_quantity(self, product, var, event=None): + try: + quantity = int(var.get()) + self.quantities[product] = quantity + self.update_total() + except ValueError: + var.set(str(self.quantities.get(product, 0))) + + def update_total(self): + total = sum(self.quantities[product] * self.PRICES[product] for product in self.products) # get total + self.total_label.config(text=f"Gesamt: €{total:.2f}") + + def load_deposit_history(self): + try: + with open('deposit_history.json', 'r') as f: + return json.load(f) + except FileNotFoundError: + return [] + + def save_deposit_history(self): + try: + with open('deposit_history.json', 'w') as f: + json.dump(self.deposit_history, f) + except Exception as e: + messagebox.showerror("Fehler", f"Fehler beim Speichern der Historie: {str(e)}") + + # Changed in Version 7.4.101 + def show_deposit_history(self): + try: + with open("deposit_history.json", "r", encoding="utf-8") as file: + deposit_history = json.load(file) + except (FileNotFoundError, json.JSONDecodeError) as e: + print("error while loading file") + return + + history_window = tk.Toplevel(self.root) + history_window.title("Pfand Abgabe Historie") + history_window.geometry("900x500") + + main_frame = ttk.Frame(history_window) + main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + all_items = set() + for deposit in deposit_history: + all_items.update(deposit['quantities'].keys()) + + item_columns = sorted(all_items) + columns = ['Datum'] + item_columns + ['Gesamt'] + + tree = ttk.Treeview(main_frame, columns=columns, show='headings') + + for col in columns: + tree.heading(col, text=col, anchor='center') + width = 100 if col == 'Datum' else 80 + tree.column(col, width=width, anchor='center' if col != 'Gesamt' else 'e') + + scrollbar = ttk.Scrollbar(main_frame, orient=tk.VERTICAL, command=tree.yview) + tree.configure(yscrollcommand=scrollbar.set) + + tree.grid(row=0, column=0, sticky='nsew') + scrollbar.grid(row=0, column=1, sticky='ns') + + main_frame.grid_columnconfigure(0, weight=1) + main_frame.grid_rowconfigure(0, weight=1) + + totals = {item: 0 for item in item_columns} + total_amount = 0 + + for deposit in deposit_history: + quantities = deposit.get('quantities', {}) + row = [deposit.get('date', '')] + for item in item_columns: + count = quantities.get(item, 0) + row.append(count) + totals[item] += count + amount = deposit.get('total', 0.0) + row.append(f"{amount:.2f}") + total_amount += amount + tree.insert('', tk.END, values=row) + + totals_frame = ttk.Frame(main_frame) + totals_frame.grid(row=1, column=0, sticky='ew', pady=(5, 0)) + + separator = ttk.Separator(main_frame, orient='horizontal') + separator.grid(row=2, column=0, sticky='ew', pady=2) + + bold_font = ('TkDefaultFont', 9, 'bold') + + row = ["Gesamt:"] + for item in item_columns: + row.append(f"{totals[item]} {item}") + row.append(f"€{total_amount:.2f}") + for idx, value in enumerate(row): + ttk.Label(totals_frame, text=value, font=bold_font).grid(row=0, column=idx, sticky='w', padx=5) + + separator.grid(row=3, column=0, sticky='ew', pady=5) + + def make_deposit(self): + deposit_dialog = tk.Toplevel(self.root) + deposit_dialog.title("Pfand Abgeben") + deposit_dialog.geometry("300x150") + deposit_dialog.transient(self.root) + deposit_dialog.grab_set() + + ttk.Label(deposit_dialog, text="Abgabe Datum:").pack(pady=5) + date_picker = DateEntry(deposit_dialog, width=12, background='darkblue', + foreground='white', borderwidth=2, locale='de_DE') + date_picker.pack(pady=5) + + def confirm_deposit(): + date = date_picker.get_date().strftime("%d.%m.%Y") + current_total = sum(self.quantities[product] * self.PRICES[product] + for product in self.products) + + deposit_record = { + 'date': date, + 'quantities': dict(self.quantities), + 'total': current_total + } + self.deposit_history.append(deposit_record) + self.save_deposit_history() + + self.quantities = {product: 0 for product in self.products} + self.save_quantities() + self.update_total() + + for widget in self.root.winfo_children(): + if isinstance(widget, ttk.Frame): + for child in widget.winfo_children(): + if isinstance(child, ttk.Frame): + for grandchild in child.winfo_children(): + if isinstance(grandchild, ttk.Spinbox): + grandchild.set("0") + + messagebox.showinfo("Erfolg", "Pfand wurde erfolgreich abgegeben!") + deposit_dialog.destroy() + + button_frame = ttk.Frame(deposit_dialog) + button_frame.pack(pady=20) + ttk.Button(button_frame, text="Abgeben", command=confirm_deposit).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="Abbrechen", command=deposit_dialog.destroy).pack(side=tk.LEFT, padx=5) + + def quick_deposit(self): + if sum(self.quantities.values()) == 0: + messagebox.showinfo("Info", "Keine Mengen zum Abgeben vorhanden.") + return + + if messagebox.askyesno("Pfand Abgeben", "Möchten Sie den Pfand mit dem heutigen Datum abgeben?"): + current_date = datetime.now().strftime("%d.%m.%Y") + current_total = sum(self.quantities[product] * self.PRICES[product] + for product in self.products) + + deposit_record = { + 'date': current_date, + 'quantities': dict(self.quantities), + 'total': current_total + } + self.deposit_history.append(deposit_record) + self.save_deposit_history() + + self.check_achievements() + + self.quantities = {product: 0 for product in self.products} + self.save_quantities() + self.update_total() + + for widget in self.root.winfo_children(): + if isinstance(widget, ttk.Frame): + for child in widget.winfo_children(): + if isinstance(child, ttk.Frame): + for grandchild in child.winfo_children(): + if isinstance(grandchild, ttk.Spinbox): + grandchild.set("0") + + messagebox.showinfo("Erfolg", "Pfand wurde erfolgreich abgegeben!") + else: + self.make_deposit() + + def export_history_csv(self): + if not self.deposit_history: + messagebox.showinfo("Info", "Keine Historie zum Exportieren vorhanden.") + return + + try: + file_path = filedialog.asksaveasfilename( + defaultextension=".csv", + filetypes=[("CSV Dateien", "*.csv")], + initialfile="pfand_historie.csv" + ) + + if not file_path: + return + + with open(file_path, 'w', newline='', encoding='utf-8') as csvfile: + writer = csv.writer(csvfile, delimiter=';') + + # Create header with all products + header = ['Datum'] + self.products + ['Gesamt (€)'] + writer.writerow(header) + + for deposit in self.deposit_history: + # Create row with all products + row = [deposit['date']] + for product in self.products: + row.append(deposit['quantities'].get(product, 0)) + row.append(f"{deposit['total']:.2f}") + writer.writerow(row) + + messagebox.showinfo("Erfolg", "Historie wurde erfolgreich exportiert!") + except Exception as e: + messagebox.showerror("Fehler", f"Fehler beim Exportieren: {str(e)}") + + def clear_deposit_history(self): + if not self.deposit_history: + messagebox.showinfo("Info", "Keine Historie zum Löschen vorhanden.") + return + + if messagebox.askyesno("Löschen bestätigen", + "Sind Sie sicher, dass Sie die gesamte Abgabe-Historie löschen möchten?\n" + "Dieser Vorgang kann nicht rückgängig gemacht werden!"): + try: + self.deposit_history = [] + if os.path.exists('deposit_history.json'): + os.remove('deposit_history.json') + messagebox.showinfo("Erfolg", "Abgabe-Historie wurde erfolgreich gelöscht!") + except Exception as e: + messagebox.showerror("Fehler", f"Fehler beim Löschen der Historie: {str(e)}") + + def handle_shift_f1(self, event): + if event.state & 0x1: + self.remove_save_file() + + def handle_shift_f2(self, event): + if event.state & 0x1: + self.clear_deposit_history() + + def delete_achievements(self): + if not any(achievement.unlocked for achievement in self.achievements.values()): + messagebox.showinfo("Info", "Keine Auszeichnungen zum Löschen vorhanden.") + return + + if messagebox.askyesno("Löschen bestätigen", + "Sind Sie sicher, dass Sie alle Auszeichnungen löschen möchten?\n" + "Dieser Vorgang kann nicht rückgängig gemacht werden!"): + try: + for achievement in self.achievements.values(): + achievement.unlocked = False + achievement.unlock_date = None + + if os.path.exists('achievements.json'): + os.remove('achievements.json') + messagebox.showinfo("Erfolg", "Alle Auszeichnungen wurden erfolgreich gelöscht!") + except Exception as e: + messagebox.showerror("Fehler", f"Fehler beim Löschen der Auszeichnungen: {str(e)}") + + def open_scanner_window(self): + if self.scanner_window is None or not self.scanner_window.winfo_exists(): + self.scanner_window = tk.Toplevel(self.root) + self.scanner_window.title("Pfand Scanner") + self.scanner_window.protocol("WM_DELETE_WINDOW", self.close_scanner_window) + + # Create frames for scanner layout + self.camera_frame = ttk.Frame(self.scanner_window) + self.camera_frame.pack(side="left", padx=10, pady=5) + + self.scanner_control_frame = ttk.Frame(self.scanner_window) + self.scanner_control_frame.pack(side="left", padx=10, pady=5, fill="y") + + # Create camera label + self.camera_label = ttk.Label(self.camera_frame) + self.camera_label.pack() + + # Create focus control + focus_frame = ttk.LabelFrame(self.scanner_control_frame, text="Kamera Einstellungen") + focus_frame.pack(pady=5, padx=5, fill="x") + + ttk.Label(focus_frame, text="Fokus:").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="Autofokus", + variable=self.autofocus_var, + command=self.toggle_autofocus + ) + self.autofocus_check.pack(pady=2) + + # Create image processing controls + process_frame = ttk.LabelFrame(self.scanner_control_frame, text="Bildverarbeitung") + process_frame.pack(pady=5, padx=5, fill="x") + + ttk.Label(process_frame, text="Helligkeit:").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="Kontrast:").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") + + # Start/Stop button + self.scan_button = ttk.Button( + self.scanner_control_frame, + text="Scannen Starten", + command=self.toggle_scanning + ) + self.scan_button.pack(pady=10) + + # Initialize scan counter for achievements + self.daily_scans = 0 + self.total_scans = 0 + self.last_scan_date = None + + # Queue for thread-safe communication + self.queue = queue.Queue() + + # Set window size to match camera resolution + self.scanner_window.geometry("1600x800") + + def close_scanner_window(self): + if self.scanning: + self.toggle_scanning() + if self.cap: + self.cap.release() + self.cap = None + if self.scanner_window: + self.scanner_window.destroy() + self.scanner_window = None + + def toggle_scanning(self): + if not self.scanning: + self.cap = cv2.VideoCapture(0) + + # Set optimal camera properties for performance + self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) + self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) + self.cap.set(cv2.CAP_PROP_FPS, 30) # Request 30 FPS + self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # Minimize buffer delay + + self.cap.set(cv2.CAP_PROP_AUTOFOCUS, 0) + self.cap.set(cv2.CAP_PROP_FOCUS, 0) + + self.scanning = True + self.scan_button.configure(text="Scannen Stoppen") + self.process_video() + else: + self.scanning = False + if self.cap: + self.cap.release() + self.cap = None + self.scan_button.configure(text="Scannen Starten") + self.camera_label.configure(image='') + + def toggle_autofocus(self): + if self.cap: + 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): + # This method is now only used for preview adjustments + 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) + return adjusted + + def process_video(self): + if not self.scanning: + return + + try: + ret, frame = self.cap.read() + if ret: + if not self.autofocus_var.get(): + self.cap.set(cv2.CAP_PROP_FOCUS, self.focus_slider.get()) + + # Resize frame for faster processing (720p is plenty for barcode detection) + frame = cv2.resize(frame, (1280, 720)) + + # Only process every 3rd frame for barcode detection + if hasattr(self, 'frame_count'): + self.frame_count += 1 + else: + self.frame_count = 0 + + if self.frame_count % 3 == 0: # Process every 3rd frame + # Process in grayscale for better performance + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + barcodes = decode(gray) + + for barcode in barcodes: + points = barcode.polygon + if len(points) == 4: + pts = np.array([(p.x, p.y) for p in points]) + cv2.polylines(frame, [pts], True, (0, 255, 0), 2) + + barcode_data = barcode.data.decode('utf-8') + if barcode_data not in self.scanned_barcodes: + self.scanned_barcodes.add(barcode_data) + self.scanner_window.after(0, lambda d=barcode_data: self.handle_barcode(d)) + + # Convert and display frame + 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) + + if self.scanning: + # Use a shorter delay for higher frame rate + self.scanner_window.after(5, self.process_video) + except Exception as e: + print(f"Error in process_video: {e}") + if self.scanning: + self.scanner_window.after(5, self.process_video) + + def handle_barcode(self, barcode_data): + # First dialog for Pfand symbol verification + verify_dialog = tk.Toplevel(self.scanner_window) + verify_dialog.title("Pfand Symbol Überprüfung") + verify_dialog.transient(self.scanner_window) + verify_dialog.grab_set() + + ttk.Label(verify_dialog, text="Ist ein Pfand Symbol auf dem Produkt?").pack(pady=10) + + def handle_verification(has_pfand): + verify_dialog.destroy() + if has_pfand: + # Add barcode to history with timestamp and Pfand status + self.barcode_history.append({ + 'timestamp': datetime.now().strftime("%d.%m.%Y %H:%M:%S"), + 'barcode': barcode_data, + 'has_pfand': True + }) + self.show_product_selection_dialog(barcode_data) + else: + # Add barcode to history with timestamp and Pfand status + self.barcode_history.append({ + 'timestamp': datetime.now().strftime("%d.%m.%Y %H:%M:%S"), + 'barcode': barcode_data, + 'has_pfand': False + }) + self.scanned_barcodes.remove(barcode_data) + messagebox.showinfo("Kein Pfand", "Dieses Produkt hat kein Pfand Symbol.") + + button_frame = ttk.Frame(verify_dialog) + button_frame.pack(pady=10) + ttk.Button(button_frame, text="Ja", command=lambda: handle_verification(True)).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="Nein", command=lambda: handle_verification(False)).pack(side=tk.LEFT, padx=5) + + def show_product_selection_dialog(self, barcode_data): + dialog = tk.Toplevel(self.scanner_window) + dialog.title("Barcode Erkannt") + dialog.transient(self.scanner_window) + dialog.grab_set() + + ttk.Label(dialog, text=f"Barcode erkannt: {barcode_data}").pack(pady=10) + ttk.Label(dialog, text="Produkttyp auswählen:").pack(pady=5) + + product_var = tk.StringVar() + for product in self.products: + ttk.Radiobutton(dialog, text=product, variable=product_var, value=product).pack() + + def confirm(): + selected_product = product_var.get() + if selected_product: + print(f"Erhöhe {selected_product}") # Debug print + # Update quantity + current_qty = self.quantities.get(selected_product, 0) + self.quantities[selected_product] = current_qty + 1 + print(f"Neue Menge für {selected_product}: {self.quantities[selected_product]}") # Debug print + + # Update scan counters and check achievements + self.update_scan_achievements() + + # Force immediate UI update + def do_update(): + try: + # Directly update the spinbox + spinbox = self.spinboxes[selected_product] + spinbox.set(str(self.quantities[selected_product])) + spinbox.update() # Force the spinbox to update + + # Update the total + self.update_total() + self.root.update_idletasks() # Force the entire UI to update + + # Save the quantities + self.save_quantities() + print("UI Update und Speicherung abgeschlossen") # Debug print + except Exception as e: + print(f"Fehler beim UI Update: {e}") + + # Schedule the update for the next event loop iteration + self.root.after(1, do_update) + dialog.destroy() + + def skip(): + self.scanned_barcodes.remove(barcode_data) + dialog.destroy() + + button_frame = ttk.Frame(dialog) + button_frame.pack(pady=10) + ttk.Button(button_frame, text="Hinzufügen", command=confirm).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="Überspringen", command=skip).pack(side=tk.LEFT, padx=5) + + def update_scan_achievements(self): + current_date = datetime.now().strftime("%Y-%m-%d") + + # Update daily scan counter + if self.last_scan_date != current_date: + self.daily_scans = 1 + self.last_scan_date = current_date + else: + self.daily_scans += 1 + + # Update total scan counter + self.total_scans += 1 + + # Check scan-related achievements + achievements_to_check = { + 1: "first_scan", + 50: "scans_50", + 100: "scans_100", + 500: "scans_500" + } + + # Check total scans achievements + for count, achievement_key in achievements_to_check.items(): + if self.total_scans == count and not self.achievements[achievement_key].unlocked: + self.unlock_achievement(achievement_key) + self.save_achievements() + + # Check daily scan achievements + daily_achievements = { + 10: "daily_10", + 25: "daily_25" + } + + for count, achievement_key in daily_achievements.items(): + if self.daily_scans == count and not self.achievements[achievement_key].unlocked: + self.unlock_achievement(achievement_key) + self.save_achievements() + + # Save the updated scan counts + self.save_quantities() # This ensures we don't lose progress + + def update_ui(self): + def update_spinboxes(): + try: + # Update all spinboxes to match quantities + main_frame = self.root.winfo_children()[0] # Get the main frame + for frame in main_frame.winfo_children(): + if isinstance(frame, ttk.Frame): + # Find the product this frame represents + for widget in frame.winfo_children(): + if isinstance(widget, ttk.Label) and widget.cget('text') in self.products: + product = widget.cget('text') + current_qty = self.quantities.get(product, 0) + # Find and update the spinbox + for w in frame.winfo_children(): + if isinstance(w, ttk.Spinbox): + w.set(str(current_qty)) + w.update() + break + + # Update the total + self.update_total() + except Exception as e: + print(f"Error updating UI: {e}") + + # Ensure updates happen in the main thread + if threading.current_thread() is threading.main_thread(): + update_spinboxes() + else: + self.root.after(0, update_spinboxes) + + def on_closing(self): + if self.scanner_window and self.scanner_window.winfo_exists(): + self.close_scanner_window() + self.root.destroy() + + def export_barcodes_csv(self): + if not self.barcode_history: + messagebox.showinfo("Info", "Keine Barcodes zum Exportieren vorhanden.") + return + + try: + file_path = filedialog.asksaveasfilename( + defaultextension=".csv", + filetypes=[("CSV Dateien", "*.csv")], + initialfile="barcode_historie.csv" + ) + + if not file_path: + return + + with open(file_path, 'w', newline='', encoding='utf-8') as csvfile: + writer = csv.writer(csvfile, delimiter=';') + + # Write header + writer.writerow(['Datum', 'Barcode', 'Hat Pfand']) + + # Write barcode history + for entry in self.barcode_history: + writer.writerow([ + entry['timestamp'], + entry['barcode'], + 'Ja' if entry['has_pfand'] else 'Nein' + ]) + + messagebox.showinfo("Erfolg", "Barcode Historie wurde erfolgreich exportiert!") + except Exception as e: + messagebox.showerror("Fehler", f"Fehler beim Exportieren: {str(e)}") + + def load_products(self): + try: + with open('products.json', 'r') as f: + data = json.load(f) + self.products = data.get('products', []) + self.PRICES = data.get('prices', {}) + except FileNotFoundError: + # Default products if no JSON exists + self.products = ["Flaschen", "Bierflasche", "Kasten", "Dose", "Plastikflasche", "Monster", "Joghurt Glas"] + self.PRICES = { + "Flaschen": 0.25, + "Bierflasche": 0.20, + "Kasten": 3.00, + "Dose": 0.25, + "Plastikflasche": 0.25, + "Monster": 0.25, + "Joghurt Glas": 0.17, + } + self.save_products() + + def save_products(self): + try: + data = { + 'products': self.products, + 'prices': self.PRICES + } + with open('products.json', 'w') as f: + json.dump(data, f) + except Exception as e: + messagebox.showerror("Fehler", f"Fehler beim Speichern der Produkte: {str(e)}") + + def show_add_product_window(self): + dialog = tk.Toplevel(self.root) + dialog.title("Neues Produkt hinzufügen") + dialog.geometry("400x300") + dialog.transient(self.root) + dialog.grab_set() + + # Product name + name_frame = ttk.Frame(dialog) + name_frame.pack(fill='x', padx=10, pady=5) + ttk.Label(name_frame, text="Produktname:").pack(side='left') + name_var = tk.StringVar() + name_entry = ttk.Entry(name_frame, textvariable=name_var) + name_entry.pack(side='left', fill='x', expand=True, padx=5) + + # Deposit amount + deposit_frame = ttk.Frame(dialog) + deposit_frame.pack(fill='x', padx=10, pady=5) + ttk.Label(deposit_frame, text="Pfandbetrag (€):").pack(side='left') + deposit_var = tk.StringVar() + deposit_entry = ttk.Entry(deposit_frame, textvariable=deposit_var) + deposit_entry.pack(side='left', fill='x', expand=True, padx=5) + + # Image selection + image_frame = ttk.Frame(dialog) + image_frame.pack(fill='x', padx=10, pady=5) + ttk.Label(image_frame, text="Bild:").pack(side='left') + image_path_var = tk.StringVar() + image_entry = ttk.Entry(image_frame, textvariable=image_path_var) + image_entry.pack(side='left', fill='x', expand=True, padx=5) + ttk.Button(image_frame, text="Durchsuchen", command=lambda: self.select_image(image_path_var)).pack(side='left') + + def add_product(): + name = name_var.get().strip() + try: + deposit = float(deposit_var.get().replace(',', '.')) + except ValueError: + messagebox.showerror("Fehler", "Bitte geben Sie einen gültigen Pfandbetrag ein.") + return + + if not name: + messagebox.showerror("Fehler", "Bitte geben Sie einen Produktnamen ein.") + return + + if name in self.products: + messagebox.showerror("Fehler", "Ein Produkt mit diesem Namen existiert bereits.") + return + + image_path = image_path_var.get() + if image_path: + try: + # Create images directory if it doesn't exist + if not os.path.exists('PfandApplication/images'): + os.makedirs('images') + + # Copy and rename the image + new_image_path = f"PfandApplication/images/{name.lower()}.png" + shutil.copy2(image_path, new_image_path) + except Exception as e: + messagebox.showerror("Fehler", f"Fehler beim Kopieren des Bildes: {str(e)}") + return + + # Add the new product + self.products.append(name) + self.PRICES[name] = deposit + self.save_products() + + # Update the UI + self.recreate_widgets() + dialog.destroy() + + ttk.Button(dialog, text="Hinzufügen", command=add_product).pack(pady=10) + + def select_image(self, image_path_var, preview_label=None): + file_path = filedialog.askopenfilename( + filetypes=[("PNG files", "*.png"), ("All files", "*.*")] + ) + if file_path: + image_path_var.set(file_path) + if preview_label: + try: + image = Image.open(file_path) + # Resize image to fit preview (100x100) + image.thumbnail((100, 100), Image.Resampling.LANCZOS) + photo = ImageTk.PhotoImage(image) + preview_label.configure(image=photo) + preview_label.image = photo # Keep a reference + except Exception as e: + print(f"Error loading preview: {e}") + + def show_manage_products_window(self): + dialog = tk.Toplevel(self.root) + dialog.title("Produkte verwalten") + dialog.geometry("1200x600") + dialog.transient(self.root) + dialog.grab_set() + + # Create main container with two columns + main_container = ttk.Frame(dialog) + main_container.pack(fill='both', expand=True, padx=10, pady=5) + + # Left column for product list + left_frame = ttk.Frame(main_container) + left_frame.pack(side='left', fill='both', expand=True, padx=(0, 5)) + + # Right column for add/edit product + right_frame = ttk.LabelFrame(main_container, text="Produkt hinzufügen") + right_frame.pack(side='right', fill='both', expand=True, padx=(5, 0)) + + # Create treeview in left frame + tree = ttk.Treeview(left_frame, columns=('Name', 'Pfand'), show='headings') + tree.heading('Name', text='Name') + tree.heading('Pfand', text='Pfand (€)') + tree.pack(fill='both', expand=True) + + # Add scrollbar for treeview + scrollbar = ttk.Scrollbar(left_frame, orient='vertical', command=tree.yview) + scrollbar.pack(side='right', fill='y') + tree.configure(yscrollcommand=scrollbar.set) + + # Populate treeview + for product in self.products: + tree.insert('', 'end', values=(product, f"{self.PRICES[product]:.2f}")) + + # Add product form in right frame + # Product name + name_frame = ttk.Frame(right_frame) + name_frame.pack(fill='x', padx=10, pady=5) + ttk.Label(name_frame, text="Produktname:").pack(side='left') + name_var = tk.StringVar() + name_entry = ttk.Entry(name_frame, textvariable=name_var) + name_entry.pack(side='left', fill='x', expand=True, padx=5) + + # Deposit amount + deposit_frame = ttk.Frame(right_frame) + deposit_frame.pack(fill='x', padx=10, pady=5) + ttk.Label(deposit_frame, text="Pfandbetrag (€):").pack(side='left') + deposit_var = tk.StringVar() + deposit_entry = ttk.Entry(deposit_frame, textvariable=deposit_var) + deposit_entry.pack(side='left', fill='x', expand=True, padx=5) + + # Image selection with preview + image_frame = ttk.Frame(right_frame) + image_frame.pack(fill='x', padx=10, pady=5) + ttk.Label(image_frame, text="Bild:").pack(side='left') + image_path_var = tk.StringVar() + image_entry = ttk.Entry(image_frame, textvariable=image_path_var) + image_entry.pack(side='left', fill='x', expand=True, padx=5) + ttk.Button(image_frame, text="Durchsuchen", command=lambda: self.select_image(image_path_var, preview_label)).pack(side='left') + + # Image preview + preview_frame = ttk.Frame(right_frame) + preview_frame.pack(fill='x', padx=10, pady=5) + preview_label = ttk.Label(preview_frame) + preview_label.pack() + + def update_preview(image_path): + if image_path and os.path.exists(image_path): + try: + image = Image.open(image_path) + # Resize image to fit preview (100x100) + image.thumbnail((100, 100), Image.Resampling.LANCZOS) + photo = ImageTk.PhotoImage(image) + preview_label.configure(image=photo) + preview_label.image = photo # Keep a reference + except Exception as e: + print(f"Error loading preview: {e}") + + def select_image_with_preview(image_path_var, preview_label): + file_path = filedialog.askopenfilename( + filetypes=[("PNG files", "*.png"), ("All files", "*.*")] + ) + if file_path: + image_path_var.set(file_path) + update_preview(file_path) + + def add_product(): + name = name_var.get().strip() + try: + deposit = float(deposit_var.get().replace(',', '.')) + except ValueError: + messagebox.showerror("Fehler", "Bitte geben Sie einen gültigen Pfandbetrag ein.") + return + + if not name: + messagebox.showerror("Fehler", "Bitte geben Sie einen Produktnamen ein.") + return + + if name in self.products: + messagebox.showerror("Fehler", "Ein Produkt mit diesem Namen existiert bereits.") + return + + image_path = image_path_var.get() + if image_path: + try: + # Create images directory if it doesn't exist + if not os.path.exists('PfandApplication/images'): + os.makedirs('images') + + # Copy and rename the image + new_image_path = f"PfandApplication/images/{name.lower()}.png" + shutil.copy2(image_path, new_image_path) + except Exception as e: + messagebox.showerror("Fehler", f"Fehler beim Kopieren des Bildes: {str(e)}") + return + + # Add the new product + self.products.append(name) + self.PRICES[name] = deposit + self.save_products() + + # Update treeview + tree.insert('', 'end', values=(name, f"{deposit:.2f}")) + tree.yview_moveto(1) # Scroll to the bottom to show the new item + + # Clear form + name_var.set("") + deposit_var.set("") + image_path_var.set("") + preview_label.configure(image='') + + # Update the main window UI + self.recreate_widgets() + + def delete_product(): + selected = tree.selection() + if not selected: + messagebox.showwarning("Warnung", "Bitte wählen Sie ein Produkt aus.") + return + + if messagebox.askyesno("Bestätigen", "Möchten Sie das ausgewählte Produkt wirklich löschen?"): + item = tree.item(selected[0]) + product_name = item['values'][0] + + # Remove from lists + self.products.remove(product_name) + del self.PRICES[product_name] + + # Delete image if exists + image_path = f"PfandApplication/images/{product_name.lower()}.png" + if os.path.exists(image_path): + try: + os.remove(image_path) + except Exception as e: + print(f"Fehler beim Löschen des Bildes: {e}") + + # Save changes and update UI + self.save_products() + self.recreate_widgets() + dialog.destroy() + + # Add buttons + button_frame = ttk.Frame(right_frame) + button_frame.pack(fill='x', padx=10, pady=10) + ttk.Button(button_frame, text="Hinzufügen", command=add_product).pack(side='left', padx=5) + ttk.Button(button_frame, text="Löschen", command=delete_product).pack(side='left', padx=5) + + # Bind image selection to preview update + image_path_var.trace_add('write', lambda *args: update_preview(image_path_var.get())) + + def recreate_widgets(self): + # Clear existing widgets + for widget in self.root.winfo_children(): + widget.destroy() + + # Recreate menu + self.create_menu() + + # Reload quantities + self.load_quantities() + + # Recreate main widgets + self.create_widgets() + + @staticmethod + def launch(check_for_update): + root = tk.Tk() + app = PfandCalculator(root) + + if check_for_update == True: + root.after(1, run_silent_update) # Run uc on start (1s delay) => updater.py module || UNCOMMENT IN PROD + else: + pass + + root.mainloop() + +if __name__ == "__main__": + PfandCalculator.launch(True) diff --git a/PfandApplication/pfand_scanner.py b/PfandApplication/pfand_scanner.py new file mode 100644 index 0000000..d1ca5be --- /dev/null +++ b/PfandApplication/pfand_scanner.py @@ -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") diff --git a/PfandApplication/updater.py b/PfandApplication/updater.py new file mode 100644 index 0000000..d4dd296 --- /dev/null +++ b/PfandApplication/updater.py @@ -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( + "", + 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("", 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() diff --git a/PfandApplication/wiki/__pycache__/main.cpython-313.pyc b/PfandApplication/wiki/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000..a0fbf13 Binary files /dev/null and b/PfandApplication/wiki/__pycache__/main.cpython-313.pyc differ diff --git a/PfandApplication/wiki/listeHOFER.csv b/PfandApplication/wiki/listeHOFER.csv new file mode 100644 index 0000000..17df5c7 --- /dev/null +++ b/PfandApplication/wiki/listeHOFER.csv @@ -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, diff --git a/PfandApplication/wiki/listeSPAR.csv b/PfandApplication/wiki/listeSPAR.csv new file mode 100644 index 0000000..ac1f734 --- /dev/null +++ b/PfandApplication/wiki/listeSPAR.csv @@ -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" diff --git a/PfandApplication/wiki/main.py b/PfandApplication/wiki/main.py new file mode 100644 index 0000000..f8c001c --- /dev/null +++ b/PfandApplication/wiki/main.py @@ -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'

(.*?)

|\*\*(.*?)\*\*', 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() + diff --git a/PfandApplication/wiki/readme.md b/PfandApplication/wiki/readme.md new file mode 100644 index 0000000..4a44415 --- /dev/null +++ b/PfandApplication/wiki/readme.md @@ -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! diff --git a/PfandApplication/wiki/wiki.md b/PfandApplication/wiki/wiki.md new file mode 100644 index 0000000..cd39d0f --- /dev/null +++ b/PfandApplication/wiki/wiki.md @@ -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. + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..46762f9 --- /dev/null +++ b/README.md @@ -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. diff --git a/iex.png b/iex.png new file mode 100644 index 0000000..32ef074 Binary files /dev/null and b/iex.png differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8e1fc5b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +Pillow +tgtg +requests +tkcalendar +opencv-python +pyzbar +numpy diff --git a/run.py b/run.py new file mode 100644 index 0000000..d960e7a --- /dev/null +++ b/run.py @@ -0,0 +1,4 @@ +from PfandApplication import main + +if __name__ == "__main__": + main.PfandCalculator.launch(False) #Debug False