import csv import os import sys import tkinter as tk from tkinter import font, filedialog from configparser import NoSectionError, NoOptionError from configparser import ConfigParser from tkinter import messagebox from tkinter import ttk import json def show_error(message_title: str, message: str, parent: tk.Tk | tk.Toplevel): messagebox.showwarning( title=message_title, message=message, parent=parent ) class Window(tk.Toplevel): def __init__(self, root: tk.Tk): super().__init__(root) self.root = root self.protocol("WM_DELETE_WINDOW", self.close_window) def close_window(self): self.destroy_window() def destroy_window(self): self.root.update() self.destroy() class InsertRecord(Window): def __init__(self, root: tk.Tk): super().__init__(root) self.title("Adresse einfügen") tk.Button(self, text="Abbrechen", command=self.close_window).pack() class SettingsWindow(Window): def __init__(self, root: tk.Tk): super().__init__(root) self.geometry(f"500x330+{self.root.winfo_x() + 20}+{self.root.winfo_y() + 20}") self.config = Config() self.json_file = tk.StringVar() self.csv_file = tk.StringVar() title = font.Font(family='Ubuntu Mono', size=20, weight=font.BOLD) tk.Label(self, text="Einstellungen", font=title).pack(pady=20) path_frame = tk.Frame(self) path_frame.pack(pady=(10, 40)) tk.Label(path_frame, text="Datenpfad JSON Datei").grid(row=0, column=0) tk.Entry(path_frame, textvariable=self.json_file, width=50).grid(row=1, column=0) tk.Button(path_frame, text="Pfad", command=lambda: self.set_json_path(self.json_file.get())).grid(row=1, column=1) tk.Label(path_frame, text="Datenpfad CSV Export Datei").grid(row=2, column=0) tk.Entry(path_frame, textvariable=self.csv_file, width=50).grid(row=3, column=0) tk.Button(path_frame, text="Pfad", command=lambda: self.set_csv_path(self.csv_file.get())).grid(row=3, column=1) bottom_frame = tk.Frame(self) bottom_frame.pack() tk.Button(bottom_frame, text="Ok", command=self.ok).pack(side=tk.LEFT) tk.Button(bottom_frame, text="Abbrechen", command=self.cancel).pack(side=tk.LEFT) self.load_config() def set_json_path(self, initial_dir: str = ""): initial_dir = "~/" if initial_dir == "" else initial_dir new_path = filedialog.askdirectory(initialdir=initial_dir, title="Datenpfad JSON Datei") new_path = initial_dir if len(new_path) == 0 else new_path self.json_file.set(new_path) def set_csv_path(self, initial_dir: str = ""): initial_dir = "~/" if initial_dir == "" else initial_dir new_path = filedialog.askdirectory(initialdir=initial_dir, title="Datenpfad CSV Datei") new_path = initial_dir if len(new_path) == 0 else new_path self.csv_file.set(new_path) def load_config(self): try: self.json_file.set(self.config.get("json", "path")) except NoSectionError: self.config.add_section("json") self.config.set("json", "path", "") except NoOptionError: self.config.set("json", "path", "") try: self.csv_file.set(self.config.get("csv", "path")) except NoSectionError: self.config.add_section("csv") self.config.set("csv", "path", "") except NoOptionError: self.config.set("csv", "path", "") def ok(self): if self.json_file.get() == "" or self.csv_file.get() == "": show_error(message_title="Fehlerhafte Konfiguration", message="Pfad für JSON oder CSV Datei fehlt", parent=self) return self.config.set("json", "path", self.json_file.get()) self.config.set("csv", "path", self.csv_file.get()) self.close_window() def cancel(self): self.close_window() class Config: parser: ConfigParser def __init__(self): """ Config parser reading config.ini Attributes: self.parser: ConfigParser holding list of sections and options self.__filename: Path and name to the config file """ self.parser = ConfigParser() self.config_file_name = "config.ini" self.root_path = os.path.dirname(os.path.abspath(__file__)) self.config_file = os.path.join(self.root_path, self.config_file_name) self._load() def _save(self): with open(self.config_file, 'w') as outfile: self.parser.write(outfile) def _load(self): self.parser.read(self.config_file) def add_section(self, section): self._load() self.parser.add_section(section) self._save() def set(self, section: str, option: str, value: str): self._load() self.parser.set(section, option, value) self._save() def get(self, section: str, option: str): self._load() return self.parser.get(section, option) class Application: def __init__(self): # tkinter settings x_offset = 700 y_offset = 200 width = 1050 height = 700 title = "Brovski Adress-Etiketten Verwaltung" self.root = tk.Tk(className="BrovskiAdressEtiketten") self.root.title(title) self.root.protocol("WM_DELETE_WINDOW", self.on_close) self.root.geometry(f"{width}x{height}+{x_offset}+{y_offset}") self.config = Config() self.config_good = False # variables self.address_list = [] self.current_record: int | None = None self.sort_order = False # init paths to json and csv file self.json_file_name = "brovski-adress-etiketten-verwaltung.json" self.csv_file_name = "brovski-adress-etiketten.csv" self.json_path = "" self.csv_path = "" self.load_config() self.json_file = os.path.join(self.json_path, self.json_file_name) self.csv_file = os.path.join(self.csv_path, self.csv_file_name) # leave application if settings are bad if not self.config_good: show_error(message_title="Fehler Konfiguration", message="Die Konfiguration ist fehlerhaft, bitte prüfe deine config.ini", parent=self.root ) sys.exit() top_frame = tk.Frame(self.root) top_frame.pack(side=tk.TOP, fill=tk.X) button_width = 8 tk.Button(top_frame, text="Insert", command=self.insert_record, width=button_width).pack(side=tk.LEFT) tk.Button(top_frame, text="Delete", command=self.delete_record, width=button_width).pack(side=tk.LEFT) tk.Button(top_frame, text="Export CSV", command=self.export_csv, width=button_width).pack(side=tk.LEFT) tk.Button(top_frame, text="Quit", command=self.on_close, width=button_width).pack(side=tk.RIGHT) tk.Button(top_frame, text="Settings", command=self.show_settings, width=button_width).pack(side=tk.RIGHT) self.aktiv = tk.StringVar() self.firma = tk.StringVar() self.name = tk.StringVar() self.strasse = tk.StringVar() self.plz_ort = tk.StringVar() edit_frame = tk.Frame(self.root) edit_frame.pack(side=tk.TOP, fill=tk.X) tk.Label(edit_frame, text="Aktiv").grid(row=0, column=0) tk.Label(edit_frame, text="Firma").grid(row=0, column=1) tk.Label(edit_frame, text="Name").grid(row=0, column=2) tk.Label(edit_frame, text="Strasse").grid(row=0, column=3) tk.Label(edit_frame, text="Plz/Ort").grid(row=0, column=4) edit_aktiv = tk.Checkbutton(edit_frame, variable=self.aktiv, onvalue="x", offvalue="") edit_aktiv.grid(row=1, column=0) edit_firma = tk.Entry(edit_frame, textvariable=self.firma) edit_firma.grid(row=1, column=1) edit_name = tk.Entry(edit_frame, textvariable=self.name) edit_name.grid(row=1, column=2) edit_strasse = tk.Entry(edit_frame, textvariable=self.strasse) edit_strasse.grid(row=1, column=3) edit_plz_ort = tk.Entry(edit_frame, textvariable=self.plz_ort) edit_plz_ort.grid(row=1, column=4) tk.Button(edit_frame, text="Update", command=self.update_record, width=button_width).grid(row=1, column=5) tk.Button(edit_frame, text="Toggle Aktiv", command=self.toggle_active, width=button_width).grid(row=1, column=6) data_frame = tk.Frame(self.root, bg="teal") data_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True) scrollbar = ttk.Scrollbar(data_frame, orient=tk.VERTICAL) self.table = ttk.Treeview(data_frame, yscrollcommand=scrollbar.set, columns=("0", "1", "2", "3", "4"), show="headings") scrollbar.config(command=self.table.yview) self.table.heading('0', text="Aktiv") self.table.column('0', anchor=tk.CENTER, width=0) self.table.heading('1', text="Firma") self.table.heading('2', text="Name") self.table.heading('3', text="Strasse") self.table.heading('4', text="Plz/Ort") self.table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar.pack(side=tk.LEFT, fill=tk.Y) self.table.bind("", self.mouse_click) self._load_json_file() self.root.mainloop() def load_config(self): try: self.json_path = self.config.get("json", "path") self.csv_path = self.config.get("csv", "path") if self.json_path == "" or self.csv_path == "": raise ValueError("Empty JSON or CSV path") self.config_good = True except NoSectionError: self.show_config_error() self.show_settings() except NoOptionError: self.show_config_error() self.show_settings() except ValueError: self.show_config_error() self.show_settings() def show_config_error(self): show_error(message_title="Fehlerhafte Konfiguration", message="Konnte benötigte Parameter in config.ini nicht finden", parent=self.root) def on_close(self): self.root.destroy() def show_settings(self): settings = self.open_window(SettingsWindow) settings.wait_window() def insert_record(self): self.open_window(InsertRecord) if self.current_record is not None: self.clear_entry_fields() children = self.table.get_children() values = [ "x", "Firma", "Name", "Strasse", "Plz/Ort", ] self.table.insert('', 'end', values=values) self._save_json_file() def delete_record(self): if self.current_record is None: return if messagebox.askyesno( "Eintrag löschen?", "Willst du diesen Eintrag wirklich löschen?\nDies kann nicht rückgängig gemacht werden"): self.table.delete(self.current_record) self.clear_entry_fields() self.deselect_tree() self._save_json_file() def update_record(self): if self.current_record is None: return values = { 0: self.aktiv.get(), 1: self.firma.get(), 2: self.name.get(), 3: self.strasse.get(), 4: self.plz_ort.get(), } for key, value in values.items(): self.table.set(self.current_record, key, value) self.clear_entry_fields() self._save_json_file() def toggle_active(self): items = self.table.selection() if len(items) == 0: return for record_id in items: values = self.table.item(record_id, "values") active = values[0] new_active = "x" if active == "" else "" self.table.set(record_id, "0", new_active) self.clear_entry_fields() self._save_json_file() def clear_entry_fields(self): entry_var_list = [self.aktiv, self.firma, self.name, self.strasse, self.plz_ort] for entry in entry_var_list: entry.set("") self.current_record = None def deselect_tree(self): while len(self.table.selection()) > 0: self.table.selection_remove(self.table.selection()[0]) def mouse_click(self, event): region = self.table.identify("region", event.x, event.y) match region: case "heading": self.click_on_header(event) case "cell": self.click_on_cell() def click_on_header(self, event): column = self.table.identify_column(event.x) match column: case "#1": self.address_list.sort(key=lambda x: x[int(column[-1]) - 1], reverse=self.sort_order) self.populate_table() case "#2": self.address_list.sort(key=lambda x: x[int(column[-1]) - 1], reverse=self.sort_order) self.populate_table() case "#3": self.address_list.sort(key=lambda x: x[int(column[-1]) - 1], reverse=self.sort_order) self.populate_table() case "#4": self.address_list.sort(key=lambda x: x[int(column[-1]) - 1], reverse=self.sort_order) self.populate_table() case "#5": self.address_list.sort(key=lambda x: x[int(column[-1]) - 1], reverse=self.sort_order) self.populate_table() case _: print(column) self.sort_order = not self.sort_order self.clear_entry_fields() def click_on_cell(self): self.current_record = self.table.focus() values = self.table.item(self.current_record, "values") entry_var_list = [self.aktiv, self.firma, self.name, self.strasse, self.plz_ort] for i in range(len(values)): entry_var_list[i].set(values[i]) def _load_json_file(self): try: with open(self.json_file, "r", encoding="utf-8") as f: self.address_list = json.load(f) self.address_list.sort(key=lambda x: (x[0], x[1])) except FileNotFoundError: show_error( message_title="Datei nicht gefunden", message=f"{self.json_file_name} nicht gefunden, erstelle leere Datei unter {self.json_path}", parent=self.root ) self.address_list = [["", "firma", "name", "adresse", "plz/ort"]] with open(self.json_file, "w", encoding="utf-8") as f: json.dump(self.address_list, f) self.populate_table() def _save_json_file(self): self.export_table_to_address_list() try: with open(self.json_file, "w", encoding="utf-8") as f: json.dump(self.address_list, f, indent=4, sort_keys=True) except FileNotFoundError: show_error( message_title="Unexpected Error: File not found?!", message=f"{self.json_file_name} not found", parent=self.root ) def export_csv(self): try: with open(self.csv_file, "w", encoding="utf-8") as f: writer = csv.writer(f, delimiter=",") for address in self.address_list: if address[0] != "x": continue del address[0] if address[0] == "": del address[0] writer.writerow(address) except FileNotFoundError: show_error(message_title="Unexpected error", message=f"Could not write file {self.csv_file}", parent=self.root ) def populate_table(self): self.delete_all_table_items() for index, item in enumerate(self.address_list): self.table.insert('', 'end', iid=index, values=item) def export_table_to_address_list(self): self.address_list.clear() for child in self.table.get_children(): self.address_list.append([]) for value in self.table.item(child)['values']: self.address_list[-1].append(value) def delete_all_table_items(self): for item in self.table.get_children(): self.table.delete(item) def open_window(self, child: type[Window]) -> Window: window = child(self.root) window.wm_transient(self.root) window.grab_set() return window if __name__ == '__main__': Application()