20 Commits

Author SHA1 Message Date
3299584924 up version to 0.2.1b: added absender to configuration window 2025-09-30 00:31:21 +02:00
0869bb96f8 have compile exit if a command fails 2025-09-30 00:28:27 +02:00
89a86ca271 implemented config absender to csv export 2025-09-30 00:26:41 +02:00
6410853d3c added absender to config window 2025-09-30 00:23:10 +02:00
f993220ad6 up version to 0.2.0b 2025-09-27 12:47:35 +02:00
dac342f3ec fix target dir can be missing 2025-09-27 12:47:19 +02:00
61ab5a9deb fix import and paths 2025-09-27 12:46:57 +02:00
b61c52972d added amount of labels per print 2025-09-27 12:05:32 +02:00
c80a587cbd inject connector instead of coupling 2025-06-17 18:16:05 +02:00
0d114a325d updated to version 0.1.1b 2025-05-10 02:31:12 +02:00
9532eb7247 new version numbers with three digits 2025-05-10 02:30:45 +02:00
7e9d5bec9b Merge pull request 'handle-empty-datafile' (#14) from handle-empty-datafile into main
Reviewed-on: #14
2025-05-10 02:27:33 +02:00
6407cf4229 fixed bug #12 2025-05-09 21:28:39 +02:00
9ad45ed63f bash syntax fix 2025-05-09 21:27:47 +02:00
3224c1c2aa Merge pull request 'fix_model_structure' (#13) from fix_model_structure into main
Reviewed-on: #13
2025-05-04 16:33:22 +02:00
ca88c4d229 compiled 0.1.0b beta version for release 2025-05-04 16:30:50 +02:00
12d0d7034f refactored model, fixed utf-8 export json 2025-05-04 16:22:56 +02:00
2d840d6ad4 Merge pull request 'Statuszeile und filtern von aktiven Adressen' (#9) from statuszeile_und_filter into main
Reviewed-on: #9
2025-04-30 20:04:41 +02:00
0ae2c7e559 new version 0.7a, and update json filename for new format 2025-04-30 20:00:20 +02:00
96995f1e80 fixed keybindings, refactoring and table focus 2025-04-30 19:58:16 +02:00
8 changed files with 150 additions and 48 deletions

View File

@@ -1,5 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
if [ $VIRTUAL_ENV=="" ]
set -e
if [ "$VIRTUAL_ENV" == "" ]
then then
source venv/bin/activate source venv/bin/activate
fi fi
@@ -11,10 +14,11 @@ if [ "$new_version" != "" ]
then then
echo "$new_version" | tee version.txt echo "$new_version" | tee version.txt
fi fi
sed -i "s/VERSION = '[0-9]\.[0-9]\w'/VERSION = '$(cat version.txt)'/g" src/brovski-adress-etiketten-verwaltung.py sed -i "s/VERSION = '[0-9]\.[0-9].[0-9]\w'/VERSION = '$(cat version.txt)'/g" src/brovski-adress-etiketten-verwaltung.py
sed -i "s/Version: [0-9]\.[0-9]\w/Version: $(cat version.txt)/g" deb-package/brovski-adressetiketten/DEBIAN/control sed -i "s/Version: [0-9]\.[0-9].[0-9]\w/Version: $(cat version.txt)/g" deb-package/brovski-adressetiketten/DEBIAN/control
pyinstaller --clean --onefile src/brovski-adress-etiketten-verwaltung.py pyinstaller --clean --onefile src/brovski-adress-etiketten-verwaltung.py
mkdir -p deb-package/brovski-adressetiketten/usr/local/bin
cp dist/brovski-adress-etiketten-verwaltung deb-package/brovski-adressetiketten/usr/local/bin/brovski-adressetiketten cp dist/brovski-adress-etiketten-verwaltung deb-package/brovski-adressetiketten/usr/local/bin/brovski-adressetiketten
dpkg-deb --build deb-package/brovski-adressetiketten dpkg-deb --build deb-package/brovski-adressetiketten
mv deb-package/brovski-adressetiketten.deb ./ mv deb-package/brovski-adressetiketten.deb ./

View File

@@ -1,5 +1,5 @@
Package: brovski-adressetiketten Package: brovski-adressetiketten
Version: 0.6a Version: 0.2.1b
Maintainer: Ovski Maintainer: Ovski
Architecture: all Architecture: all
Description: manage csv files for glables address labels Description: manage csv files for glables address labels

View File

@@ -5,21 +5,22 @@ import tkinter as tk
from configparser import NoSectionError, NoOptionError from configparser import NoSectionError, NoOptionError
from tkinter import messagebox from tkinter import messagebox
from tkinter import ttk from tkinter import ttk
import json
from config import Config from config import Config
from model import Model
from connector import JSONConnector from connector import JSONConnector
from windows import SettingsWindow, EditRecord, Window, show_error from windows import SettingsWindow, EditRecord, show_error
class Application: class Application:
def __init__(self): def __init__(self):
os.chdir(os.getcwd())
# tkinter settings # tkinter settings
x_offset = 700 x_offset = 700
y_offset = 200 y_offset = 200
width = 1050 width = 1050
height = 700 height = 700
VERSION = '0.6a' VERSION = '0.2.1b'
title = f"Brovski Adress-Etiketten Verwaltung {VERSION}" title = f"Brovski Adress-Etiketten Verwaltung {VERSION}"
self.root = tk.Tk(className="BrovskiAdressEtiketten") self.root = tk.Tk(className="BrovskiAdressEtiketten")
@@ -37,7 +38,7 @@ class Application:
self.filter_active = tk.BooleanVar(value=False) self.filter_active = tk.BooleanVar(value=False)
# model connector # model connector
self.model = JSONConnector() self.model = Model(JSONConnector(self.config))
# init paths to json and csv file # init paths to json and csv file
self.json_file_name = "brovski-adress-etiketten-verwaltung.json" self.json_file_name = "brovski-adress-etiketten-verwaltung.json"
@@ -80,7 +81,7 @@ class Application:
# table content # table content
scrollbar = ttk.Scrollbar(data_frame, orient=tk.VERTICAL) scrollbar = ttk.Scrollbar(data_frame, orient=tk.VERTICAL)
self.table = ttk.Treeview(data_frame, yscrollcommand=scrollbar.set, columns=("0", "1", "2", "3", "4"), self.table = ttk.Treeview(data_frame, yscrollcommand=scrollbar.set, columns=("0", "1", "2", "3", "4", "5"),
show="headings") show="headings")
scrollbar.config(command=self.table.yview) scrollbar.config(command=self.table.yview)
self.table.heading('0', text="Aktiv") self.table.heading('0', text="Aktiv")
@@ -89,12 +90,15 @@ class Application:
self.table.heading('2', text="Name") self.table.heading('2', text="Name")
self.table.heading('3', text="Strasse") self.table.heading('3', text="Strasse")
self.table.heading('4', text="Plz/Ort") self.table.heading('4', text="Plz/Ort")
self.table.heading('5', text="Anzahl")
self.table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) self.table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.LEFT, fill=tk.Y) scrollbar.pack(side=tk.LEFT, fill=tk.Y)
self.table.bind("<ButtonRelease-1>", self.mouse_click) self.table.bind("<ButtonRelease-1>", self.mouse_click)
self.table.bind("<Return>", self.mouse_click_double) self.table.bind("<Return>", self.enter_button)
self.table.bind("<Double-1>", self.mouse_click_double) self.table.bind("<Double-1>", self.mouse_click_double)
self.root.bind("<Up>", self.focus_table)
self.root.bind("<Down>", self.focus_table)
# bottom status bar # bottom status bar
tk.Label(bottom_frame, textvariable=self.statusbar).pack(side=tk.LEFT) tk.Label(bottom_frame, textvariable=self.statusbar).pack(side=tk.LEFT)
@@ -137,7 +141,8 @@ class Application:
"firma": "Firma", "firma": "Firma",
"name": "Name", "name": "Name",
"strasse": "Strasse", "strasse": "Strasse",
"plzort": "Plz/Ort" "plzort": "Plz/Ort",
"anzahl": "Anzahl"
} }
self.model.create_new(values) self.model.create_new(values)
self.populate_table() self.populate_table()
@@ -202,7 +207,10 @@ class Application:
region = self.table.identify("region", event.x, event.y) region = self.table.identify("region", event.x, event.y)
match region: match region:
case "cell": case "cell":
self.click_on_cell() self.edit_selected_record()
def enter_button(self, event):
self.edit_selected_record()
def click_on_header(self, event): def click_on_header(self, event):
column = self.table.identify_column(event.x) column = self.table.identify_column(event.x)
@@ -223,13 +231,17 @@ class Application:
field = "strasse" field = "strasse"
case "#5": case "#5":
field = "plzort" field = "plzort"
case "#6":
field = "anzahl"
case _: case _:
field = "name" field = "name"
self.address_list = self.model.get_all_sorted_by(field, self.sort_order) self.address_list = self.model.get_all_sorted_by(field, self.sort_order)
self.populate_table(reload=False) self.populate_table(reload=False)
def click_on_cell(self): def edit_selected_record(self):
if self.table.focus() is None or self.table.focus() == "":
return
self.current_record = int(self.table.focus()) self.current_record = int(self.table.focus())
self.open_window_edit_records(self.current_record) self.open_window_edit_records(self.current_record)
@@ -241,6 +253,7 @@ class Application:
for address in self.address_list: for address in self.address_list:
if address["aktiv"] != "x": if address["aktiv"] != "x":
continue continue
for index in range(int(address["anzahl"])):
line = [] line = []
if address["firma"] != "": if address["firma"] != "":
line.append(address["firma"]) line.append(address["firma"])
@@ -248,6 +261,11 @@ class Application:
line.append(address["strasse"]) line.append(address["strasse"])
line.append(address["plzort"]) line.append(address["plzort"])
writer.writerow(line) writer.writerow(line)
# todo: add "absender" to config parameters
line = []
for idx in range(4):
line.append(self.config.get("absender", f"{idx}"))
writer.writerow(line)
except FileNotFoundError: except FileNotFoundError:
show_error(message_title="Unexpected error", show_error(message_title="Unexpected error",
message=f"Could not write file {self.csv_file}", message=f"Could not write file {self.csv_file}",
@@ -258,6 +276,7 @@ class Application:
if reload: if reload:
self.address_list = self.model.get_all() self.address_list = self.model.get_all()
if len(self.address_list) == 0: if len(self.address_list) == 0:
self.delete_all_table_items()
return return
self.delete_all_table_items() self.delete_all_table_items()
for address in self.address_list: for address in self.address_list:
@@ -266,7 +285,7 @@ class Application:
continue continue
self.table.insert('', 'end', iid=address["record_id"], self.table.insert('', 'end', iid=address["record_id"],
values=(address["aktiv"], address["firma"], address["name"], address["strasse"], values=(address["aktiv"], address["firma"], address["name"], address["strasse"],
address["plzort"]) address["plzort"], address["anzahl"])
) )
self.update_status_bar() self.update_status_bar()
@@ -283,14 +302,14 @@ class Application:
self.table.delete(item) self.table.delete(item)
def open_window_settings(self): def open_window_settings(self):
window = SettingsWindow(self, self.root) window = SettingsWindow(self, self.root, self.config)
window.wm_transient(self.root) window.wm_transient(self.root)
window.wait_visibility() window.wait_visibility()
window.grab_set() window.grab_set()
return window return window
def open_window_edit_records(self, record_id: int): def open_window_edit_records(self, record_id: int):
window = EditRecord(self, self.root, record_id) window = EditRecord(self, self.root, record_id, self.config)
window.wm_transient(self.root) window.wm_transient(self.root)
window.wait_visibility() window.wait_visibility()
window.grab_set() window.grab_set()
@@ -311,6 +330,16 @@ class Application:
self.address_list.sort(key=lambda x: (x["firma"], x["name"])) self.address_list.sort(key=lambda x: (x["firma"], x["name"]))
self.populate_table() self.populate_table()
def focus_table(self, event):
first = self.table.get_children()[0]
last = self.table.get_children()[-1]
goto = last if event.keysym == "Up" else first
if self.table.focus() == "":
self.table.selection_set(goto)
self.table.focus(goto)
self.table.see(goto)
self.table.focus_force()
if __name__ == '__main__': if __name__ == '__main__':
Application() Application()

View File

@@ -1,11 +1,11 @@
import os import os
from configparser import ConfigParser from configparser import ConfigParser, NoOptionError, NoSectionError, DuplicateSectionError
class Config: class Config:
parser: ConfigParser parser: ConfigParser
def __init__(self): def __init__(self, filename: str = "config.ini"):
""" """
Config parser reading config.ini Config parser reading config.ini
@@ -19,7 +19,7 @@ class Config:
full_path = os.path.join(home_path, ".config", "brovski-adress-etiketten" ) full_path = os.path.join(home_path, ".config", "brovski-adress-etiketten" )
if not os.path.exists(full_path): if not os.path.exists(full_path):
os.makedirs(full_path) os.makedirs(full_path)
self.config_file = os.path.join(full_path, "config.ini") self.config_file = os.path.join(full_path, filename)
self._load() self._load()
@@ -32,14 +32,25 @@ class Config:
def add_section(self, section): def add_section(self, section):
self._load() self._load()
try:
self.parser.add_section(section) self.parser.add_section(section)
except DuplicateSectionError:
return
self._save() self._save()
def set(self, section: str, option: str, value: str): def set(self, section: str, option: str, value: str):
self._load() self._load()
self.add_section(section)
self.parser.set(section, option, value) self.parser.set(section, option, value)
self._save() self._save()
def get(self, section: str, option: str): def get(self, section: str, option: str):
self._load() self._load()
return self.parser.get(section, option) try:
option = self.parser.get(section, option)
except NoOptionError:
option = ""
except NoSectionError:
option = ""
return option

View File

@@ -2,12 +2,12 @@ import json
import os import os
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
import config from config import Config
class Connector(ABC): class Connector(ABC):
def __init__(self): def __init__(self, config: Config):
pass self.config = config
@abstractmethod @abstractmethod
def get_all(self) -> list: def get_all(self) -> list:
@@ -29,17 +29,23 @@ class Connector(ABC):
def create_new(self, values: dict) -> int: def create_new(self, values: dict) -> int:
pass pass
@abstractmethod
def get_all_sorted_by(self, field: str, reverse: bool = False) -> list:
pass
class JSONConnector(Connector): class JSONConnector(Connector):
def __init__(self): def __init__(self, config: Config):
super().__init__() super().__init__(config)
self.config = config.Config()
self.json_path = self.config.get("json", "path") self.json_path = self.config.get("json", "path")
self.json_file = os.path.join(self.json_path, "brovski-adress-etiketten-verwaltung.json") self.json_file = os.path.join(self.json_path, "brovski-adress-etiketten-verwaltung-v7.json")
def get_all(self) -> list: def get_all(self) -> list:
try:
with open(self.json_file, "r") as f: with open(self.json_file, "r") as f:
return json.load(f) return json.load(f)
except FileNotFoundError:
return []
def get_all_sorted_by(self, field: str, reverse=False) -> list: def get_all_sorted_by(self, field: str, reverse=False) -> list:
with open(self.json_file, "r") as f: with open(self.json_file, "r") as f:
@@ -78,5 +84,5 @@ class JSONConnector(Connector):
return next_id return next_id
def _write_to_file(self, data): def _write_to_file(self, data):
with open(self.json_file, "w") as f: with open(self.json_file, "w", encoding="UTF-8") as f:
json.dump(data, f, indent=4) json.dump(data, f, indent=4, ensure_ascii=False)

24
src/model.py Normal file
View File

@@ -0,0 +1,24 @@
from connector import Connector
class Model:
def __init__(self, connector: Connector):
self.connector = connector
def get_all(self):
return self.connector.get_all()
def get_all_sorted_by(self, field: str, reverse: bool = False):
return self.connector.get_all_sorted_by(field=field, reverse=reverse)
def get_by_id(self, record_id: int):
return self.connector.get_by_id(record_id)
def delete_by_id(self, record_id: int):
self.connector.delete_by_id(record_id)
def update_record(self, new_record: dict):
self.connector.update_record(new_record)
def create_new(self, record: dict):
self.connector.create_new(record)

View File

@@ -32,11 +32,11 @@ class Window(tk.Toplevel):
class EditRecord(Window): class EditRecord(Window):
def __init__(self, parent, root: tk.Tk, record_id: int): def __init__(self, parent, root: tk.Tk, record_id: int, config: Config):
super().__init__(parent, root) super().__init__(parent, root)
self.bind("<Return>", self._update) self.bind("<Return>", self._update)
self.model = JSONConnector() self.model = JSONConnector(config)
record = self.model.get_by_id(record_id) record = self.model.get_by_id(record_id)
@@ -48,6 +48,7 @@ class EditRecord(Window):
self.name = tk.StringVar(value=record.get("name")) self.name = tk.StringVar(value=record.get("name"))
self.strasse = tk.StringVar(value=record.get("strasse")) self.strasse = tk.StringVar(value=record.get("strasse"))
self.plz_ort = tk.StringVar(value=record.get("plzort")) self.plz_ort = tk.StringVar(value=record.get("plzort"))
self.anzahl = tk.StringVar(value=record.get("anzahl"))
edit_frame = tk.Frame(self) edit_frame = tk.Frame(self)
edit_frame.pack(side=tk.TOP, fill=tk.X, pady=20, padx=20) edit_frame.pack(side=tk.TOP, fill=tk.X, pady=20, padx=20)
@@ -56,6 +57,7 @@ class EditRecord(Window):
tk.Label(edit_frame, text="Name").grid(row=0, column=2) 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="Strasse").grid(row=0, column=3)
tk.Label(edit_frame, text="Plz/Ort").grid(row=0, column=4) tk.Label(edit_frame, text="Plz/Ort").grid(row=0, column=4)
tk.Label(edit_frame, text="Anzahl").grid(row=0, column=5)
edit_aktiv = tk.Checkbutton(edit_frame, variable=self.aktiv, onvalue="x", offvalue="") edit_aktiv = tk.Checkbutton(edit_frame, variable=self.aktiv, onvalue="x", offvalue="")
edit_aktiv.grid(row=1, column=0) edit_aktiv.grid(row=1, column=0)
edit_firma = tk.Entry(edit_frame, textvariable=self.firma) edit_firma = tk.Entry(edit_frame, textvariable=self.firma)
@@ -66,6 +68,8 @@ class EditRecord(Window):
edit_strasse.grid(row=1, column=3) edit_strasse.grid(row=1, column=3)
edit_plz_ort = tk.Entry(edit_frame, textvariable=self.plz_ort) edit_plz_ort = tk.Entry(edit_frame, textvariable=self.plz_ort)
edit_plz_ort.grid(row=1, column=4) edit_plz_ort.grid(row=1, column=4)
edit_anzahl = tk.Entry(edit_frame, textvariable=self.anzahl)
edit_anzahl.grid(row=1, column=5)
button_frame = tk.Frame(self) button_frame = tk.Frame(self)
button_frame.pack(side=tk.TOP, pady=10) button_frame.pack(side=tk.TOP, pady=10)
@@ -80,26 +84,32 @@ class EditRecord(Window):
"name": self.name.get(), "name": self.name.get(),
"strasse": self.strasse.get(), "strasse": self.strasse.get(),
"plzort": self.plz_ort.get(), "plzort": self.plz_ort.get(),
"anzahl": self.anzahl.get(),
} }
self.model.update_record(new_record) self.model.update_record(new_record)
self.close_window() self.close_window()
class SettingsWindow(Window): class SettingsWindow(Window):
def __init__(self, parent, root: tk.Tk): def __init__(self, parent, root: tk.Tk, config: Config):
super().__init__(parent, root) super().__init__(parent, root)
self.geometry(f"500x330+{self.root.winfo_x() + 20}+{self.root.winfo_y() + 20}") width = 500
self.config = Config() height = 630
self.geometry(f"{width}x{height}+{self.root.winfo_x() + 20}+{self.root.winfo_y() + 20}")
self.config = config
self.json_file = tk.StringVar() self.json_file = tk.StringVar()
self.csv_file = tk.StringVar() self.csv_file = tk.StringVar()
self.absender_line = [tk.StringVar() for idx in range(4)]
title = font.Font(family='Ubuntu Mono', size=20, weight=font.BOLD) title = font.Font(family='Ubuntu Mono', size=20, weight=font.BOLD)
tk.Label(self, text="Einstellungen", font=title).pack(pady=20) 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.Label(self, text="Datenpfad JSON Datei").pack()
path_frame = tk.Frame(self)
path_frame.pack(pady=(10, 10))
tk.Entry(path_frame, textvariable=self.json_file, width=50).grid(row=1, 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, tk.Button(path_frame, text="Pfad", command=lambda: self.set_json_path(self.json_file.get())).grid(row=1,
column=1) column=1)
@@ -108,6 +118,13 @@ class SettingsWindow(Window):
tk.Entry(path_frame, textvariable=self.csv_file, width=50).grid(row=3, 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) tk.Button(path_frame, text="Pfad", command=lambda: self.set_csv_path(self.csv_file.get())).grid(row=3, column=1)
absender_frame = tk.Frame(self)
absender_frame.pack(pady=(10, 40))
tk.Label(absender_frame, text="Absender").pack(side=tk.TOP)
for idx in range(4):
tk.Entry(absender_frame, textvariable=self.absender_line[idx], width=50).pack(side=tk.TOP)
bottom_frame = tk.Frame(self) bottom_frame = tk.Frame(self)
bottom_frame.pack() bottom_frame.pack()
tk.Button(bottom_frame, text="Ok", command=self.ok).pack(side=tk.LEFT) tk.Button(bottom_frame, text="Ok", command=self.ok).pack(side=tk.LEFT)
@@ -144,6 +161,15 @@ class SettingsWindow(Window):
except NoOptionError: except NoOptionError:
self.config.set("csv", "path", "") self.config.set("csv", "path", "")
for idx in range(4):
try:
self.absender_line[idx].set(self.config.get(f"absender", f"{idx}"))
except NoSectionError:
self.config.add_section(f"line")
self.config.set("line", f"{idx}", "")
except NoOptionError:
self.config.set("line", f"{idx}", "")
def ok(self): def ok(self):
if self.json_file.get() == "" or self.csv_file.get() == "": if self.json_file.get() == "" or self.csv_file.get() == "":
show_error(message_title="Fehlerhafte Konfiguration", show_error(message_title="Fehlerhafte Konfiguration",
@@ -152,6 +178,8 @@ class SettingsWindow(Window):
return return
self.config.set("json", "path", self.json_file.get()) self.config.set("json", "path", self.json_file.get())
self.config.set("csv", "path", self.csv_file.get()) self.config.set("csv", "path", self.csv_file.get())
for i in range(4):
self.config.set("absender", f"{i}", f"{self.absender_line[i].get()}")
self.close_window() self.close_window()
def cancel(self): def cancel(self):

View File

@@ -1 +1 @@
0.6a 0.2.1b