7 Commits

11 changed files with 81 additions and 121 deletions

View File

@@ -1,22 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
if [ $VIRTUAL_ENV=="" ]
set -e
if [ "$VIRTUAL_ENV" == "" ]
then then
if [ ! -d "./venv/bin" ]; then
echo "venv not found, trying to create one"
python3 -m venv venv
fi
source venv/bin/activate source venv/bin/activate
fi fi
if [[ ! $(pip3 freeze | grep pyinstaller) ]];
then
echo "pyinstaller not found"
pip3 install -r requirements.txt
fi
version=$(cat version.txt) version=$(cat version.txt)
echo "current version set to: $version" echo "current version set to: $version"
new_version="" new_version=""
@@ -25,11 +11,10 @@ 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].[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" src/brovski-adress-etiketten-verwaltung.py
sed -i "s/Version: [0-9]\.[0-9].[0-9]\w/Version: $(cat version.txt)/g" deb-package/brovski-adressetiketten/DEBIAN/control sed -i "s/Version: [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.2.2b Version: 0.1.0b
Maintainer: Ovski Maintainer: Ovski
Architecture: all Architecture: all
Description: Manage and export addresses to csv. Can be used with glabels (example included in the source). Description: manage csv files for glables address labels

View File

@@ -1,5 +0,0 @@
altgraph==0.17.4
packaging==25.0
pyinstaller==6.16.0
pyinstaller-hooks-contrib==2025.9
setuptools==80.9.0

View File

@@ -1,4 +1,3 @@
import _csv
import csv import csv
import os import os
import sys import sys
@@ -9,19 +8,17 @@ from tkinter import ttk
from config import Config from config import Config
from model import Model from model import Model
from connector import JSONConnector
from windows import SettingsWindow, EditRecord, 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.2.2b' VERSION = '0.1.0b'
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")
@@ -39,7 +36,7 @@ class Application:
self.filter_active = tk.BooleanVar(value=False) self.filter_active = tk.BooleanVar(value=False)
# model connector # model connector
self.model = Model(JSONConnector(self.config)) self.model = Model(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"
@@ -82,7 +79,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", "5"), self.table = ttk.Treeview(data_frame, yscrollcommand=scrollbar.set, columns=("0", "1", "2", "3", "4"),
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")
@@ -91,7 +88,6 @@ 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)
@@ -142,8 +138,7 @@ 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()
@@ -232,8 +227,6 @@ class Application:
field = "strasse" field = "strasse"
case "#5": case "#5":
field = "plzort" field = "plzort"
case "#6":
field = "anzahl"
case _: case _:
field = "name" field = "name"
@@ -254,35 +247,23 @@ 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 = []
self.write_sender_to_csv(address, writer) if address["firma"] != "":
self.write_receiver_to_csv(address, writer) line.append(address["firma"])
line.append(address["name"])
line.append(address["strasse"])
line.append(address["plzort"])
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}",
parent=self.root parent=self.root
) )
def write_receiver_to_csv(self, address: dict, csv_writer: _csv.writer):
receiver_line = []
if address["firma"] != "":
receiver_line.append(address["firma"])
receiver_line.append(address["name"])
receiver_line.append(address["strasse"])
receiver_line.append(address["plzort"])
csv_writer.writerow(receiver_line)
def write_sender_to_csv(self, address: dict, csv_writer: _csv.writer):
sender_line = []
for idx in range(4):
sender_line.append(self.config.get("absender", f"{idx}"))
csv_writer.writerow(sender_line)
def populate_table(self, reload=True): def populate_table(self, reload=True):
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:
@@ -291,7 +272,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["anzahl"]) address["plzort"])
) )
self.update_status_bar() self.update_status_bar()
@@ -308,14 +289,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, self.config) window = SettingsWindow(self, self.root)
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, self.config) window = EditRecord(self, self.root, record_id)
window.wm_transient(self.root) window.wm_transient(self.root)
window.wait_visibility() window.wait_visibility()
window.grab_set() window.grab_set()

View File

@@ -1,11 +1,12 @@
import configparser
import os import os
from configparser import ConfigParser, NoOptionError, NoSectionError, DuplicateSectionError from configparser import ConfigParser, DuplicateSectionError
class Config: class Config:
parser: ConfigParser parser: ConfigParser
def __init__(self, filename: str = "config.ini"): def __init__(self, path: str = None, filename: str = "config.ini"):
""" """
Config parser reading config.ini Config parser reading config.ini
@@ -14,12 +15,18 @@ class Config:
self.__filename: Path and name to the config file self.__filename: Path and name to the config file
""" """
self.parser = ConfigParser() self.parser = ConfigParser()
self.path = path
self.filename = filename
if self.path is None:
home_path = os.environ["HOME"] home_path = os.environ["HOME"]
full_path = os.path.join(home_path, ".config", "brovski-adress-etiketten" ) full_path = os.path.join(home_path, ".config", "brovski-adress-etiketten" )
else:
full_path = self.path
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, filename) self.config_file = os.path.join(full_path, self.filename)
self._load() self._load()
@@ -35,8 +42,7 @@ class Config:
try: try:
self.parser.add_section(section) self.parser.add_section(section)
except DuplicateSectionError: except DuplicateSectionError:
return pass
self._save() self._save()
def set(self, section: str, option: str, value: str): def set(self, section: str, option: str, value: str):
@@ -47,10 +53,4 @@ class Config:
def get(self, section: str, option: str): def get(self, section: str, option: str):
self._load() self._load()
try: return self.parser.get(section, option)
option = self.parser.get(section, option)
except NoOptionError:
option = ""
except NoSectionError:
option = ""
return option

View File

@@ -1,7 +1,6 @@
import json import json
import os import os
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from config import Config from config import Config
@@ -35,17 +34,14 @@ class Connector(ABC):
class JSONConnector(Connector): class JSONConnector(Connector):
def __init__(self, config: Config): def __init__(self, config: Config, ):
super().__init__(config) super().__init__(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-v7.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:

View File

@@ -1,9 +1,9 @@
from connector import Connector from connector import Connector, JSONConnector
from config import Config
class Model: class Model:
def __init__(self, connector: Connector): def __init__(self, config: Config):
self.connector = connector self.connector = JSONConnector(config)
def get_all(self): def get_all(self):
return self.connector.get_all() return self.connector.get_all()

View File

@@ -3,7 +3,7 @@ from configparser import NoSectionError, NoOptionError
from tkinter import font, filedialog, messagebox from tkinter import font, filedialog, messagebox
from config import Config from config import Config
from connector import JSONConnector from model import Model
def show_error(message_title: str, message: str, parent: tk.Tk | tk.Toplevel): def show_error(message_title: str, message: str, parent: tk.Tk | tk.Toplevel):
@@ -18,6 +18,7 @@ class Window(tk.Toplevel):
def __init__(self, parent, root: tk.Tk): def __init__(self, parent, root: tk.Tk):
super().__init__(root) super().__init__(root)
self.parent = parent self.parent = parent
self.config = parent.config
self.root = root self.root = root
self.protocol("WM_DELETE_WINDOW", self.close_window) self.protocol("WM_DELETE_WINDOW", self.close_window)
self.bind("<Escape>", self.close_window) self.bind("<Escape>", self.close_window)
@@ -32,11 +33,11 @@ class Window(tk.Toplevel):
class EditRecord(Window): class EditRecord(Window):
def __init__(self, parent, root: tk.Tk, record_id: int, config: Config): def __init__(self, parent, root: tk.Tk, record_id: int):
super().__init__(parent, root) super().__init__(parent, root)
self.bind("<Return>", self._update) self.bind("<Return>", self._update)
self.model = JSONConnector(config) self.model = Model(self.config)
record = self.model.get_by_id(record_id) record = self.model.get_by_id(record_id)
@@ -48,7 +49,6 @@ 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)
@@ -57,7 +57,6 @@ 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)
@@ -68,8 +67,6 @@ 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)
@@ -84,32 +81,26 @@ 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, config: Config): def __init__(self, parent, root: tk.Tk):
super().__init__(parent, root) super().__init__(parent, root)
width = 500 self.geometry(f"500x330+{self.root.winfo_x() + 20}+{self.root.winfo_y() + 20}")
height = 630 self.config = Config()
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)
tk.Label(self, text="Datenpfad JSON Datei").pack()
path_frame = tk.Frame(self) path_frame = tk.Frame(self)
path_frame.pack(pady=(10, 10)) 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.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)
@@ -118,13 +109,6 @@ 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)
@@ -161,15 +145,6 @@ 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",
@@ -178,8 +153,6 @@ 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):

0
tests/__init__.py Normal file
View File

30
tests/test_config.py Normal file
View File

@@ -0,0 +1,30 @@
import os
from typing import assert_type
import pytest
from src.config import Config
@pytest.fixture
def config() -> Config:
config = Config(path="testfiles", filename="configtest.ini")
return config
def teardown_config():
print("tearing down config test")
def test_construction(config):
assert_type(config, Config)
def test_file_creation(config):
config._save()
assert os.path.isfile(os.path.join(config.path, config.filename))
def test_add_section(config):
config.add_section("test_section")
assert "test_section" in config.parser.sections()
def test_set_and_get(config):
config.set(section="section", option="option", value="value")
assert config.get(section="section", option="option") == "value"

View File

@@ -1 +1 @@
0.2.2b 0.1.0b