commit d5a8c8e9f43de3aa06feaa507c98db839a133cf6 Author: Sebastian Serfling Date: Mon Feb 17 16:16:53 2025 +0100 First Upload diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/Shopware-API.iml b/.idea/Shopware-API.iml new file mode 100644 index 0000000..99560ea --- /dev/null +++ b/.idea/Shopware-API.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..1c944f7 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..a446cdf --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..d843f34 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Lieferschein_17.02.2025.pdf b/Lieferschein_17.02.2025.pdf new file mode 100644 index 0000000..9b0ec3b --- /dev/null +++ b/Lieferschein_17.02.2025.pdf @@ -0,0 +1,74 @@ +%PDF-1.3 +% ReportLab Generated PDF document http://www.reportlab.com +1 0 obj +<< +/F1 2 0 R /F2 3 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Contents 8 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 7 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +5 0 obj +<< +/PageMode /UseNone /Pages 7 0 R /Type /Catalog +>> +endobj +6 0 obj +<< +/Author (anonymous) /CreationDate (D:20250217154506+01'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20250217154506+01'00') /Producer (ReportLab PDF Library - www.reportlab.com) + /Subject (unspecified) /Title (Lieferschein 17.02.2025) /Trapped /False +>> +endobj +7 0 obj +<< +/Count 1 /Kids [ 4 0 R ] /Type /Pages +>> +endobj +8 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 582 +>> +stream +Gau0@b>,r/&A7TLHNYUG]sd6[,GK.N!mq9+>ccM4TI(f%'mr2_*c5S"LJ"4-OlFS[B%Ji95hnf/k9:/@9]>o<.4jI7Kaq]./*IUPY]eVQb)0,ekVrZN._a#+HBE3_ei$>K5.CPg8Bp0<=_#mOWiRQ,I*i0=rL\4qVjb[P=F?@[bAN\2MF%R.Crp@Cm[*FR5(Z]/+]K(;PQf7OMKX4Ksh(9.a00pk]=5\0^fWFpJKKEO32"5Yjc)XQg9>GrZR/)_>NP43kO/5Q&+WXS9endstream +endobj +xref +0 9 +0000000000 65535 f +0000000073 00000 n +0000000114 00000 n +0000000221 00000 n +0000000333 00000 n +0000000536 00000 n +0000000604 00000 n +0000000915 00000 n +0000000974 00000 n +trailer +<< +/ID +[<52cc85184a6e7bde8ef927cb4ba48ccd><52cc85184a6e7bde8ef927cb4ba48ccd>] +% ReportLab generated PDF document -- digest (http://www.reportlab.com) + +/Info 6 0 R +/Root 5 0 R +/Size 9 +>> +startxref +1646 +%%EOF diff --git a/check_api.py b/check_api.py new file mode 100644 index 0000000..88588b7 --- /dev/null +++ b/check_api.py @@ -0,0 +1,108 @@ +import os +import customtkinter as ctk +import winreg +from PIL import Image +import webbrowser + +REG_PATH = r"Software\ShopwareDeliver" + +def rerun(): + """Löscht Registry-Werte und ruft run() auf, falls Werte fehlen""" + try: + with winreg.OpenKey(winreg.HKEY_CURRENT_USER, REG_PATH, 0, winreg.KEY_SET_VALUE) as key: + all_deleted = True # Flag, um zu prüfen, ob alle Werte gelöscht wurden + + for name in ["CLIENT_ID", "CLIENT_SECRET", "API_URL"]: + try: + winreg.DeleteValue(key, name) + print(f"✅ {name} wurde gelöscht.") + except FileNotFoundError: + print(f"⚠️ {name} war nicht vorhanden.") + all_deleted = False # Mindestens ein Wert fehlte bereits + + if all_deleted: + print("✅ Alle Werte erfolgreich gelöscht.") + run() # Falls alles erfolgreich gelöscht wurde, rufe run() auf + else: + print("⚠️ Einige Werte waren nicht vorhanden, run() wird nicht aufgerufen.") + + except FileNotFoundError: + print("⚠️ Fehler: Registry-Schlüssel nicht gefunden!") + run() # Falls die gesamte Registry fehlt, rufe run() auf + + +def run(): + REG_PATH = r"Software\ShopwareDeliver" + + def get_registry_value(name): + """Liest einen Wert aus der Windows-Registry""" + try: + with winreg.OpenKey(winreg.HKEY_CURRENT_USER, REG_PATH, 0, winreg.KEY_READ) as key: + value, _ = winreg.QueryValueEx(key, name) + return value + except FileNotFoundError: + return None + + def set_registry_value(name, value): + """Speichert einen Wert in der Windows-Registry""" + with winreg.CreateKey(winreg.HKEY_CURRENT_USER, REG_PATH) as key: + winreg.SetValueEx(key, name, 0, winreg.REG_SZ, value) + + def open_link(event=None): + webbrowser.open("https://itdata-gera.de") + + # Prüfe, ob die Werte existieren + CLIENT_ID = get_registry_value("CLIENT_ID") + CLIENT_SECRET = get_registry_value("CLIENT_SECRET") + API_URL = get_registry_value("API_URL") + + # Falls Werte fehlen, öffne das Eingabe-Fenster + if not CLIENT_ID or not CLIENT_SECRET or not API_URL: + + class SecretInputApp(ctk.CTk): + def __init__(self): + super().__init__() + + self.title("API-Konfiguration") + self.geometry("400x300") + + ctk.CTkLabel(self, text="Shopware Deliver", font=("Arial", 20)).pack(pady=5) + ctk.CTkLabel(self, text="Bitte geben Sie die API-Daten ein:", font=("Arial", 16)).pack(pady=10) + + self.client_id_entry = ctk.CTkEntry(self, placeholder_text="Client ID") + self.client_id_entry.pack(pady=5, padx=10, fill="x") + + self.client_secret_entry = ctk.CTkEntry(self, placeholder_text="Client Secret", show="*") + self.client_secret_entry.pack(pady=5, padx=10, fill="x") + + self.api_url_entry = ctk.CTkEntry(self, placeholder_text="Shopware URL") + self.api_url_entry.pack(pady=5, padx=10, fill="x") + + self.save_button = ctk.CTkButton(self, text="Speichern", command=self.save_keys) + self.save_button.pack(pady=20) + + def save_keys(self): + client_id = self.client_id_entry.get() + client_secret = self.client_secret_entry.get() + api_url = self.api_url_entry.get() + + if client_id and client_secret and api_url: + # Speichere die Werte in der Windows-Registry + set_registry_value("CLIENT_ID", client_id) + set_registry_value("CLIENT_SECRET", client_secret) + set_registry_value("API_URL", api_url) + + self.destroy() # Fenster schließen nach dem Speichern + else: + ctk.CTkLabel(self, text="Alle Felder müssen ausgefüllt sein!", text_color="red").pack(pady=5) + + # Starte die GUI + app = SecretInputApp() + app.mainloop() + + # Jetzt sind die Werte sicher gespeichert und können im restlichen Skript genutzt werden + CLIENT_ID = get_registry_value("CLIENT_ID") + CLIENT_SECRET = get_registry_value("CLIENT_SECRET") + API_URL = get_registry_value("API_URL") + + return CLIENT_ID, CLIENT_SECRET, API_URL \ No newline at end of file diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..9845063 Binary files /dev/null and b/logo.png differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..f82b420 --- /dev/null +++ b/main.py @@ -0,0 +1,342 @@ +import tkinter +from PIL import Image +import requests +import webbrowser +import customtkinter as ctk +from tkinter import messagebox +from tkinter import ttk +from reportlab.lib.pagesizes import A4 +from reportlab.pdfgen import canvas +from datetime import datetime +import datetime +import check_api + +STATUS_PAID = "in_progress" +keys = check_api.run() +CLIENT_ID = keys[0] +CLIENT_SECRET = keys[1] +SHOPWARE_URL = keys[2] + +current_order_id = None + +def get_order_line_items(access_token, order_id): + headers = { + "sw-access-key": CLIENT_ID, + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + } + items_url = f"{SHOPWARE_URL}/api/order/{order_id}/line-items" + response = requests.get(items_url, headers=headers) + response.raise_for_status() + return response.json()["data"] + +def get_order_customer(access_token, customer_id): + headers = { + "sw-access-key": CLIENT_ID, + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + } + customer_url = f"{SHOPWARE_URL}/api/order-customer/{customer_id}" + response = requests.get(customer_url, headers=headers) + response.raise_for_status() + return response.json()["data"] + +def get_order_billing_address(access_token, order_id): + headers = { + "sw-access-key": CLIENT_ID, + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + } + address_url = f"{SHOPWARE_URL}/api/order/{order_id}/billing-address/" + response = requests.get(address_url, headers=headers) + response.raise_for_status() + return response.json()["data"][0] + +def get_access_token(): + token_url = f"{SHOPWARE_URL}/api/oauth/token" + data = { + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "grant_type": "client_credentials" + } + response = requests.post(token_url, data=data) + response.raise_for_status() + return response.json()["access_token"] + +def get_paid_orders(access_token): + headers = { + "sw-access-key": CLIENT_ID, + "authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + } + orders_url = f"{SHOPWARE_URL}/api/search/order" + query = { + "limit": 100, + "page": 1, + "filter": [{ + "type": "equals", + "field": "stateMachineState.technicalName", + "value": STATUS_PAID + }] + } + response = requests.post(orders_url, headers=headers, json=query) + response.raise_for_status() + return response.json()["data"] + +def get_order_details(access_token, order_id): + headers = { + "sw-access-key": CLIENT_ID, + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json" + } + items_url = f"{SHOPWARE_URL}/api/order/{order_id}/line-items" + address_url = f"{SHOPWARE_URL}/api/order/{order_id}/billing-address/" + + response_items = requests.get(items_url, headers=headers) + response_address = requests.get(address_url, headers=headers) + + response_items.raise_for_status() + response_address.raise_for_status() + + return response_items.json()["data"], response_address.json()["data"][0] + +def show_order_details(event): + global current_order_id + + selected_item = tree_orders.focus() + if not selected_item: + return + + values = tree_orders.item(selected_item, "values") + order_number = values[0] + customer_name = values[1] + + access_token = get_access_token() + orders = get_paid_orders(access_token) + selected_order = next((o for o in orders if o['attributes']['orderNumber'] == order_number), None) + + if selected_order: + order_id = selected_order['id'] + current_order_id = order_id + + product_line, billing_address = get_order_details(access_token, order_id) + + frame_orders.pack_forget() + tree_details.delete(*tree_details.get_children()) + + for product in product_line: + label = product['attributes'].get('label', 'Unbekanntes Produkt') + quantity = product['attributes']['quantity'] + price = product['attributes']['totalPrice'] + tree_details.insert("", "end", values=(label, quantity, f"{price:.2f} €")) + + label_details.configure(text=f"Bestellung: {order_number} - {customer_name}") + frame_details.pack(fill="both", expand=True) + +def back_to_orders(): + frame_details.pack_forget() + frame_orders.pack(fill="both", expand=True) + +def print_single_order(): + if not current_order_id: + messagebox.showerror("Fehler", "Keine Bestellung ausgewählt!") + return + + access_token = get_access_token() + orders = get_paid_orders(access_token) + selected_order = next((o for o in orders if o['id'] == current_order_id), None) + + if selected_order: + filename = generate_combined_pdf([selected_order], access_token) + messagebox.showinfo("Erfolg", f"Lieferschein {filename} wurde erstellt!") + else: + messagebox.showerror("Fehler", "Bestellung nicht gefunden!") + +def generate_combined_pdf(orders, access_token): + current_date = datetime.datetime.today().strftime("%d.%m.%Y") + filename = f"Lieferschein_{current_date}.pdf" + + pdf = canvas.Canvas(filename, pagesize=A4) + pdf.setTitle(f"Lieferschein {current_date}") + pdf.setFont("Helvetica-Bold", 16) + pdf.drawString(50, 800, "Sammel-Lieferschein") + + pdf.setFont("Helvetica", 12) + pdf.drawString(50, 780, f"Erstellt am: {current_date}") + + y_position = 750 + for order in orders: + try: + customer_id = order["relationships"]["orderCustomer"]["data"]["id"] + customer = get_order_customer(access_token, customer_id) + billing_address = get_order_billing_address(access_token, order["id"]) + product_line = get_order_line_items(access_token, order["id"]) + + # Bestell-Header + pdf.setFont("Helvetica-Bold", 12) + pdf.drawString(50, y_position, f"Bestellnummer: {order['attributes']['orderNumber']}") + + date_obj = datetime.datetime.strptime(order['attributes']['orderDate'], "%Y-%m-%dT%H:%M:%S.%f%z") + date_str = date_obj.strftime("%d.%m.%Y") + pdf.setFont("Helvetica", 10) + pdf.drawString(50, y_position - 20, f"Bestelldatum: {date_str}") + + # Rahmen um Lieferadresse + pdf.setStrokeColorRGB(0, 0, 0) # Schwarz + #pdf.rect(45, y_position - 110, 300, 80, stroke=1, fill=0) # Rechteck um Adresse + + # Lieferadresse + pdf.setFont("Helvetica-Bold", 10) + pdf.drawString(50, y_position - 100, "Lieferadresse:") + pdf.setFont("Helvetica", 10) + pdf.drawString(50, y_position - 120, f"{customer['attributes']['firstName']} {customer['attributes']['lastName']}") + pdf.drawString(50, y_position - 140, f"{billing_address['attributes']['street']}") + pdf.drawString(50, y_position - 160, f"{billing_address['attributes']['zipcode']} {billing_address['attributes']['city']}") + + y_position -= 180 # Platz nach Adresse + + # Produktliste + for product in product_line: + pdf.rect(50, y_position - 2, 10, 10, stroke=1, fill=0) # Checkbox für Abhaken + label = product['attributes'].get('label', 'Unbekanntes Produkt') + product_number = product['attributes']['payload'].get('productNumber', 'Keine Nummer') + quantity = product['attributes']['quantity'] + price = product['attributes']['totalPrice'] + + pdf.drawString(70, y_position, f"{label}") + pdf.drawString(250, y_position, f"{product_number}") + pdf.drawString(350, y_position, f"{quantity}") + pdf.drawString(400, y_position, f"{price:.2f} €") + + y_position -= 20 + + # Falls die Seite voll ist, neue Seite beginnen + if y_position < 100: + pdf.showPage() + y_position = 800 + + y_position -= 40 # Platz nach jeder Bestellung + + except Exception as e: + print(f"Fehler beim Erstellen des Lieferscheins für Bestellung {order['attributes']['orderNumber']}: {str(e)}") + + pdf.save() + print(f"Lieferschein gespeichert: {filename}") + + return filename + +def print_all_orders(): + access_token = get_access_token() + orders = get_paid_orders(access_token) + + if not orders: + messagebox.showinfo("Info", "Keine Bestellungen gefunden.") + return + + filename = generate_combined_pdf(orders, access_token) + messagebox.showinfo("Erfolg", f"Gesammelter Lieferschein {filename} wurde erstellt!") + +def update_table(): + access_token = get_access_token() + orders = get_paid_orders(access_token) + # Tabelle leeren + for row in tree_orders.get_children(): + tree_orders.delete(row) + + for order in orders: + order_number = order['attributes']['orderNumber'] + _, billing_address = get_order_details(access_token, order['id']) + + tree_orders.insert("", "end", values=( + order_number, + f"{billing_address['attributes']['firstName']} {billing_address['attributes']['lastName']}", + billing_address['attributes']['street'], + f"{billing_address['attributes']['zipcode']} {billing_address['attributes']['city']}" + )) + + +def open_link(event=None): + webbrowser.open("https://itdata-gera.de") + +def execute_with_error_handling(func, *args, **kwargs): + try: + func(*args, **kwargs) # Die gewünschte Funktion ausführen + except Exception as e: + root.after(0, show_error_message, str(e)) # Fehler in GUI anzeigen + +# GUI-Fehlermeldung anzeigen +def show_error_message(error_text): + tkinter.messagebox.showerror("Fehler", f"Ein Fehler ist aufgetreten:\n{error_text}") + +# GUI-Erstellung +root = ctk.CTk() +root.title("Lieferschein-Drucker") +root.resizable(False, False) + +window_width = 900 +window_height = 500 + +# Bildschirmgröße abrufen +screen_width = root.winfo_screenwidth() +screen_height = root.winfo_screenheight() + +# Position berechnen (Mitte des Bildschirms) +x_position = (screen_width // 2) - (window_width // 2) +y_position = (screen_height // 2) - (window_height // 2) + +# Fenstergröße & Position setzen +root.geometry(f"{window_width}x{window_height}+{x_position}+{y_position}") + +# **Bestellübersicht** +frame_orders = ctk.CTkFrame(root) +button_all_print = ctk.CTkButton(frame_orders, text="API ändern", command=lambda: execute_with_error_handling(check_api.rerun),fg_color="#2b2b2b",text_color="white",anchor=tkinter.RIGHT) +button_all_print.pack(pady=5,anchor="ne") + +label_orders = ctk.CTkLabel(frame_orders, text=f"Bezahlte Bestellungen - {datetime.datetime.now().strftime("%d.%m.%Y")}", font=("Helvetica", 15)) +label_orders.pack(pady=5) + +columns_orders = ("Bestellnummer", "Kunde", "Straße", "PLZ/Stadt") +tree_orders = ttk.Treeview(frame_orders, columns=columns_orders, show="headings", cursor="hand2") +for col in columns_orders: + tree_orders.heading(col, text=col) + tree_orders.column(col, width=150) +tree_orders.pack(expand=True, fill="both") +tree_orders.bind("<>", show_order_details) + +button_update = ctk.CTkButton(frame_orders, text="Bestellungen laden", command=lambda: execute_with_error_handling(update_table), fg_color="lightblue",text_color="black",anchor=tkinter.RIGHT) +button_update.pack(side="left",pady=5,padx=10) + +logo = ctk.CTkImage(dark_image=Image.open('logo.png'),size=(130,30)) +logo_label = ctk.CTkLabel(root, text="", image=logo, fg_color="#2b2b2b",cursor="hand2") +logo_label.bind("", open_link) +logo_label.place(relx=0.5, rely=1.0, anchor="s",y=-15) + +button_all_print = ctk.CTkButton(frame_orders, text="Gesammelten Lieferschein drucken", command=lambda: execute_with_error_handling(print_all_orders),fg_color="orange",text_color="black") +button_all_print.pack(side="right",pady=15, padx=10) + +frame_orders.pack(fill="both", expand=True) + +# **Detailansicht** +frame_details = ctk.CTkFrame(root) +label_details = ctk.CTkLabel(frame_details, text="", font=("Helvetica", 12)) +label_details.pack(pady=5) + +columns_details = ("Produkt", "Anzahl", "Preis") +tree_details = ttk.Treeview(frame_details, columns=columns_details, show="headings") +for col in columns_details: + tree_details.heading(col, text=col) +tree_details.pack(expand=True, fill="both") + +button_back = ctk.CTkButton(frame_details, text="Zurück", command=back_to_orders) +button_back.pack(side="left", padx=20, pady=15) + +logo_label_1 = ctk.CTkLabel(root, text="", image=logo, fg_color="#2b2b2b",cursor="hand2") +logo_label_1.bind("", open_link) +logo_label_1.place(relx=0.5, rely=1.0, anchor="s",y=-15) + +button_print = ctk.CTkButton(frame_details, text="Diese Bestellung drucken", command=print_single_order, fg_color="lightgreen", text_color="black") +button_print.pack(side="right", padx=20,pady=15) + +# Error Handling für Timeout +root.after(100, lambda: execute_with_error_handling(update_table)) +root.mainloop()