379 lines
16 KiB
Python
Executable File
379 lines
16 KiB
Python
Executable File
#!/opt/kugelstossmeeting-ticketing/venv/bin/python3
|
|
'''
|
|
TODOS
|
|
- UI aufhübschen
|
|
- Suchfunktion einbauen
|
|
- usbscanner.py journal log automatisch öffnen per Button (soll separate Shell öffnen)
|
|
'''
|
|
|
|
import sqlite3
|
|
from sqlite3 import Error
|
|
from contextlib import closing
|
|
import os
|
|
import time
|
|
from datetime import datetime
|
|
import pytz
|
|
import tkinter as tk
|
|
#from tkinter import *
|
|
from tkinter import ttk, messagebox
|
|
import tkinter.font as font
|
|
|
|
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
db_file = os.path.join(BASE_DIR, "kugelstossmeeting-prod.db")
|
|
if os.path.isfile(db_file) is False:
|
|
error = "Error: Datenbankdatei '{}' konnte nicht gefunden werden!".format(db_file)
|
|
print(error)
|
|
messagebox.showerror("Error", error)
|
|
exit(1)
|
|
if os.path.getsize(db_file) == 0:
|
|
error = "Error: Datenbankdatei '{}' ist 0 Byte groß!".format(db_file)
|
|
print(error)
|
|
messagebox.showerror("Error", error)
|
|
exit(1)
|
|
|
|
title = "Kugelstoßmeeting Rochlitz"
|
|
|
|
timeoffset = 20 #seconds
|
|
autoscroll = True
|
|
lastSelectedTicket = None #das letzte manuell ausgewählte. Müssen wir merken, sonst wird es alle 2 Sekunden deselektiert
|
|
|
|
window = tk.Tk()
|
|
#window.geometry('800x600+0+0')
|
|
window.attributes('-zoomed', True)
|
|
window.title(title)
|
|
window.iconphoto(False, tk.PhotoImage(file=os.path.join(BASE_DIR, "icon.png")))
|
|
|
|
currentScan = None
|
|
|
|
def treeview_sort_column(tv, col, reverse):
|
|
l = [(tv.set(k, col), k) for k in tv.get_children('')]
|
|
l.sort(key=lambda t: int(t[0]), reverse=reverse)
|
|
for index, (val, k) in enumerate(l):
|
|
tv.move(k, '', index)
|
|
tv.heading(col, command=lambda: treeview_sort_column(tv, col, not reverse))
|
|
|
|
|
|
def insertManualSelectionAsScan(mb):
|
|
sql = "INSERT INTO scans(unixtimestamp_created,barcode,validated,hint,unixtimestamp_checked,skipped) VALUES(?,?,?,?,?,?)"
|
|
with closing(sqlite3.connect(db_file)) as conn:
|
|
with closing(conn.cursor()) as cur:
|
|
ts = time.time_ns()
|
|
res = cur.execute(sql,(int(ts),mb,0,"Manuelle Auswahl",None,0))
|
|
print(res)
|
|
conn.commit()
|
|
|
|
|
|
def getLastScan():
|
|
#sql = "SELECT * FROM scans WHERE skipped = 0 AND validated = 0 ORDER BY unixtimestamp_created DESC LIMIT 1;"
|
|
sql = "SELECT * FROM scans WHERE (julianday('now') - julianday(CAST(unixtimestamp_created AS float) / 1e9,'unixepoch'))*24*60*60 < ? ORDER BY unixtimestamp_created DESC LIMIT 1;"
|
|
#sql = "SELECT * FROM scans ORDER BY unixtimestamp_created DESC LIMIT 1;"
|
|
with closing(sqlite3.connect(db_file)) as conn:
|
|
with closing(conn.cursor()) as cur:
|
|
res = cur.execute(sql, (timeoffset, ))
|
|
#res = cur.execute(sql)
|
|
rows = res.fetchall()
|
|
if len(rows) > 0:
|
|
return rows[-1]
|
|
else:
|
|
return []
|
|
|
|
def getValidatedCount():
|
|
sql = "SELECT COUNT(*) FROM scans WHERE validated = 1 and skipped = 0;"
|
|
with closing(sqlite3.connect(db_file)) as conn:
|
|
with closing(conn.cursor()) as cur:
|
|
res = cur.execute(sql)
|
|
rows = res.fetchall()
|
|
validated=rows[-1][0]
|
|
sql = "SELECT COUNT(*) FROM tickets;"
|
|
with closing(sqlite3.connect(db_file)) as conn:
|
|
with closing(conn.cursor()) as cur:
|
|
res = cur.execute(sql)
|
|
rows = res.fetchall()
|
|
total=rows[-1][0]
|
|
return "{}/{} eingecheckt".format(validated, total)
|
|
|
|
#Anzahl der Barcodes, die mehr als 1x eingescannt wurden (nur für Statistik)
|
|
def getScannedDuplicates():
|
|
sql = 'SELECT COUNT(*) FROM (SELECT COUNT(*) as "ct",* FROM scans GROUP BY barcode) WHERE ct > 1;'
|
|
with closing(sqlite3.connect(db_file)) as conn:
|
|
with closing(conn.cursor()) as cur:
|
|
res = cur.execute(sql)
|
|
rows = res.fetchall()
|
|
total=rows[-1][0]
|
|
return "{} Barcodes mehr als 1x gescannt".format(total)
|
|
|
|
def isAlreadyCheckedIn(barcode):
|
|
sql = "SELECT * FROM scans WHERE validated = 1 AND barcode = ? ORDER BY unixtimestamp_checked LIMIT 1;"
|
|
with closing(sqlite3.connect(db_file)) as conn:
|
|
with closing(conn.cursor()) as cur:
|
|
res = cur.execute(sql, (barcode, ))
|
|
rows = res.fetchall()
|
|
return True if len(rows) > 0 else False
|
|
|
|
def getScans(tv):
|
|
#sql = "SELECT * FROM scans WHERE skipped = 0 AND validated = 0;" # Scans, die nicht übersprungen und noch nicht validiert wurden
|
|
sql = "SELECT * FROM scans;" # alle Scans
|
|
with closing(sqlite3.connect(db_file)) as conn:
|
|
with closing(conn.cursor()) as cur:
|
|
res = cur.execute(sql)
|
|
rows = res.fetchall()
|
|
for i in tv.get_children():
|
|
tv.delete(i)
|
|
k=1
|
|
for i in rows:
|
|
unixtimestamp_created_c = datetime.fromtimestamp(int(str(i[0])[0:10]), pytz.timezone("Europe/Berlin")).strftime('%d.%m.%Y %H:%M:%S')
|
|
try:
|
|
unixtimestamp_checked_c = datetime.fromtimestamp(int(str(i[4])[0:10]), pytz.timezone("Europe/Berlin")).strftime('%d.%m.%Y %H:%M:%S')
|
|
except:
|
|
unixtimestamp_checked_c = ""
|
|
barcode = i[1]
|
|
validated = "Ja" if i[2] == 1 else "Nein"
|
|
hint = "" if i[3] == None else i[3]
|
|
#unixtimestamp_skipped = i[4]
|
|
skipped = "Ja" if i[5] == 1 else "Nein"
|
|
unixtimestamp_created = i[0]
|
|
if k % 2 == 0:
|
|
evenodd = "evenrow"
|
|
else:
|
|
evenodd = "oddrow"
|
|
tv.insert('', 1, text=k, values=(unixtimestamp_created_c, barcode, validated, hint, unixtimestamp_checked_c, skipped, unixtimestamp_created), tags=(evenodd, ))
|
|
k+=1
|
|
treeview_sort_column(tv, 6, False) #wir sortieren nach unixtimestamp_created, weil unixtimestamp_created_c einen Fehler bringt
|
|
global autoscroll
|
|
if autoscroll is True:
|
|
if len(tv.get_children()) > 0:
|
|
lastItem = tv.get_children()[-1]
|
|
tv.focus(lastItem)
|
|
tv.selection_set(lastItem)
|
|
tv.yview_moveto(1)
|
|
|
|
#Tickets
|
|
def getTickets(tv):
|
|
global lastSelectedTicket
|
|
lastSelectedTicket = tv.index(tv.focus())
|
|
|
|
#sql = "SELECT * FROM tickets;"
|
|
sql = "SELECT DISTINCT tickets.*, CASE WHEN RES.validated = 1 THEN 'Ja' ELSE 'Nein' END FROM tickets FULL JOIN (SELECT * FROM scans WHERE validated = 1 and skipped = 0) AS RES ON RES.barcode = tickets.BarcodeContent;"
|
|
with closing(sqlite3.connect(db_file)) as conn:
|
|
with closing(conn.cursor()) as cur:
|
|
res = cur.execute(sql)
|
|
rows = res.fetchall()
|
|
for i in tv.get_children():
|
|
tv.delete(i)
|
|
k=1
|
|
for i in rows:
|
|
if k % 2 == 0:
|
|
evenodd = "evenrow"
|
|
else:
|
|
evenodd = "oddrow"
|
|
tv.insert('', 1, text=k, values=(i), tags=(evenodd, ))
|
|
k+=1
|
|
treeview_sort_column(tv, 1, False)
|
|
if len(tvScans.get_children()) > 0:
|
|
global autoscroll
|
|
if autoscroll is True:
|
|
Barcode = tvScans.item(tvScans.focus())['values'][1] #get row von Scan, wo Barcode dem Barcode in Ticket entspr
|
|
col=0 #Barcode-Spalte
|
|
l = [(tvTickets.set(k, col), k) for k in tvTickets.get_children('')]
|
|
for index, (val, k) in enumerate(l):
|
|
if val == Barcode:
|
|
tv.focus(tv.get_children()[index])
|
|
tv.selection_set(tv.get_children()[index])
|
|
tv.yview_moveto(0)
|
|
tv.yview_scroll(index, "units")
|
|
else:
|
|
tv.focus(tv.get_children()[lastSelectedTicket])
|
|
tv.selection_set(tv.get_children()[lastSelectedTicket])
|
|
|
|
def validate():
|
|
if len(currentScan) > 0:
|
|
sql = "UPDATE scans set unixtimestamp_checked = ?, hint = ?, validated = 1 WHERE unixtimestamp_created = ? AND barcode = ? AND validated = 0 AND skipped = 0;"
|
|
with closing(sqlite3.connect(db_file)) as conn:
|
|
with closing(conn.cursor()) as cur:
|
|
ts = time.time_ns()
|
|
res = cur.execute(sql,(int(ts), hintTextInput.get(1.0, "end-1c"), currentScan[0],currentScan[1]))
|
|
print(res)
|
|
conn.commit()
|
|
hintTextInput.delete(1.0,tk.END) #reset hintTextInput
|
|
|
|
def skip():
|
|
if len(currentScan) > 0:
|
|
sql = "UPDATE scans set unixtimestamp_checked = ?, hint = ?, skipped = 1 WHERE unixtimestamp_created = ? AND barcode = ? AND validated = 0;"
|
|
with closing(sqlite3.connect(db_file)) as conn:
|
|
with closing(conn.cursor()) as cur:
|
|
ts = time.time_ns()
|
|
res = cur.execute(sql,(int(ts), hintTextInput.get(1.0, "end-1c"), currentScan[0],currentScan[1]))
|
|
print(res)
|
|
conn.commit()
|
|
hintTextInput.delete(1.0,tk.END) #reset hintTextInput
|
|
|
|
toggleScrollbarButton_t1 = "Umschalten zu: Ticket manuell auswählen"
|
|
toggleScrollbarButton_t2 = "Umschalten zu: Ticket automatisch per Scan auswählen (Standard)"
|
|
|
|
def manual():
|
|
if toggleScrollbarButton.config('text')[-1] == toggleScrollbarButton_t2:
|
|
global lastSelectedTicket
|
|
mBarcode = None
|
|
if len(tvScans.get_children()) > 0:
|
|
item = tvTickets.item(tvTickets.focus())
|
|
row = 0
|
|
|
|
for (val, k) in enumerate(item.values()):
|
|
row += 1
|
|
if row == 3:
|
|
mBarcode = k[0]
|
|
print(mBarcode)
|
|
insertManualSelectionAsScan(mBarcode) #insert a new "lastScan"
|
|
|
|
def ScrollToggle():
|
|
global autoscroll
|
|
if toggleScrollbarButton.config('text')[-1] == toggleScrollbarButton_t1:
|
|
toggleScrollbarButton.config(text=toggleScrollbarButton_t2)
|
|
autoscroll = False
|
|
tvTickets['selectmode'] = "browse"
|
|
else:
|
|
toggleScrollbarButton.config(text=toggleScrollbarButton_t1)
|
|
autoscroll = True
|
|
tvTickets['selectmode'] = "none"
|
|
|
|
toggleScrollbarButton = tk.Button(text=toggleScrollbarButton_t1, width=70, relief="raised", bg='pink', command=ScrollToggle)
|
|
toggleScrollbarButton.pack(padx=10, pady=5)
|
|
|
|
f1=tk.Frame(window)
|
|
f1.pack(expand=1)
|
|
|
|
tk.Label(f1, text="Die Anzeige des aktuellen Barcodes setzt sich automatisch nach {} Sekunden zurück".format(timeoffset), padx=10, pady=5).pack()
|
|
|
|
manualButton = tk.Button(f1, text="Manuelle Auswahl", command=manual)
|
|
manualButton.pack(side=tk.LEFT)
|
|
manualButton['font']=font.Font(size=35)
|
|
|
|
barcodeLabel = tk.Label(f1, text="<>", bg='#000000', fg='#00ff00', padx=10, pady=5)
|
|
barcodeLabel.config(font=(None, 35))
|
|
barcodeLabel.pack(side=tk.LEFT)
|
|
|
|
checkinButton = tk.Button(f1, text="Einchecken/entwerten", bg='red', fg='white', command=validate)
|
|
checkinButton.pack(side=tk.RIGHT)
|
|
checkinButton['font']=font.Font(size=35)
|
|
skipButton = tk.Button(f1, text="Überspringen", bg='red', fg='white', command=skip)
|
|
skipButton.pack(side=tk.RIGHT)
|
|
skipButton['font']=font.Font(size=35)
|
|
|
|
commentLabel = tk.Label(window, text="Kommentar", padx=10, pady=5)
|
|
commentLabel.config(font=(None, 15))
|
|
commentLabel.pack()
|
|
|
|
hintTextInput = tk.Text(window, height = 3, width = 100)
|
|
hintTextInput.pack()
|
|
totalValidatedLabel = tk.Label(window, text="/", fg='#008000', padx=10, pady=5, width=100)
|
|
totalValidatedLabel.config(font=(None, 35))
|
|
totalValidatedLabel.pack()
|
|
|
|
tabControl = ttk.Notebook(window)
|
|
tabScans = ttk.Frame(tabControl)
|
|
tabTickets = ttk.Frame(tabControl)
|
|
|
|
tabControl.add(tabTickets, text='Alle Tickets')
|
|
tabControl.add(tabScans, text='Alle Scans')
|
|
tabControl.pack(fill='both')
|
|
|
|
tk.Label(tabScans, text="Scans").pack()
|
|
totalDuplicateCodeScansLabel = tk.Label(tabScans, text="/", padx=10, pady=5)
|
|
totalDuplicateCodeScansLabel.pack()
|
|
tk.Label(tabTickets, text="Tickets").pack()
|
|
|
|
def updateBarcodeLabel(label, scan):
|
|
if len(scan) == 0:
|
|
labeltext="Bitte scannen!"
|
|
label.config(fg='#0000ff')
|
|
else:
|
|
barcode = scan[1]
|
|
alreadyCheckedIn = isAlreadyCheckedIn(barcode)
|
|
if alreadyCheckedIn is False:
|
|
if toggleScrollbarButton.config('text')[-1] == toggleScrollbarButton_t2:
|
|
ScrollToggle() #toggle zurück zu Standardmodus, wenn ein normaler Scan gekommen ist
|
|
labeltext="{0} ready!".format(barcode)
|
|
label.config(fg='#00ff00')
|
|
skip = scan[4]
|
|
if skip is not None:
|
|
labeltext="{0} übersprungen!".format(barcode)
|
|
label.config(fg='#ffffff')
|
|
else:
|
|
labeltext="{0} entwertet!".format(barcode)
|
|
label.config(fg='#ff0000')
|
|
label.config(text=labeltext)
|
|
|
|
tvScans = ttk.Treeview(tabScans, height=50, selectmode="browse")
|
|
tvScans["columns"] = ("c1", "c2", "c3", "c4", "c5", "c6", "c7") #eine Spalte mehr (unixtimestamp_created, welche aber ausgeblendet ist und nur zum Sortieren genutzt wird)
|
|
tvScans["displaycolumns"]=("c1", "c2", "c3", "c6", "c4", "c5")
|
|
tvScans.column("c1", width=100)
|
|
tvScans.column("c2", anchor='center', width=100)
|
|
tvScans.column("c3", anchor='center', width=100)
|
|
tvScans.column("c4", anchor='center', width=100)
|
|
tvScans.column("c5", anchor='center', width=100)
|
|
tvScans.column("c6", anchor='center', width=100)
|
|
tvScans.heading("#0", text="Nr.")
|
|
tvScans.heading("c1", text="TS Gescannt", anchor='w')
|
|
tvScans.heading("c2", text="Barcode")
|
|
tvScans.heading("c3", text="Entwertet")
|
|
tvScans.heading("c4", text="Anmerkung/Hinweise")
|
|
tvScans.heading("c5", text="TS Bearbeitet")
|
|
tvScans.heading("c6", text="Übersprungen")
|
|
tvScans.tag_configure('oddrow', background='white', font=("monospace", 10))
|
|
tvScans.tag_configure('evenrow', background='lightblue', font=("monospace", 10))
|
|
tvScansBar = ttk.Scrollbar(tabScans, orient="vertical", command=tvScans.yview)
|
|
tvScansBar.pack(side='right', fill='x')
|
|
tvScans.configure(yscrollcommand = tvScansBar.set)
|
|
tvScans.pack(fill='both')
|
|
|
|
tvTickets = ttk.Treeview(tabTickets, height=50, selectmode="none")
|
|
tvTickets["columns"] = ("c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "c10", "c11", "c12", "c13")
|
|
tvTickets["displaycolumns"]=("c1", "c3", "c6", "c7", "c8", "c9", "c10", "c11", "c12", "c13")
|
|
tvTickets.column("c1", width=100)
|
|
tvTickets.column("c2", anchor='center', width=100)
|
|
tvTickets.column("c3", anchor='center', width=100)
|
|
tvTickets.column("c4", anchor='center', width=100)
|
|
tvTickets.column("c5", anchor='center', width=100)
|
|
tvTickets.column("c6", anchor='center', width=100)
|
|
tvTickets.column("c7", anchor='center', width=100)
|
|
tvTickets.column("c8", anchor='center', width=100)
|
|
tvTickets.column("c9", anchor='center', width=100)
|
|
tvTickets.column("c10", anchor='center', width=100)
|
|
tvTickets.column("c11", anchor='center', width=100)
|
|
tvTickets.column("c12", anchor='center', width=100)
|
|
tvTickets.column("c13", anchor='center', width=100)
|
|
tvTickets.heading("#0", text="Nr.")
|
|
tvTickets.heading("c1", text="Barcode", anchor='w')
|
|
tvTickets.heading("c2", text="Nr")
|
|
tvTickets.heading("c3", text="VIP")
|
|
tvTickets.heading("c4", text="Ticketbeschriftung")
|
|
tvTickets.heading("c5", text="BarcodeFile")
|
|
tvTickets.heading("c6", text="Medium")
|
|
tvTickets.heading("c7", text="Aushändigung")
|
|
tvTickets.heading("c8", text="Bestellnummer/Verwendungszweck")
|
|
tvTickets.heading("c9", text="Bezahlt am")
|
|
tvTickets.heading("c10", text="Hinweise")
|
|
tvTickets.heading("c11", text="Besteller")
|
|
tvTickets.heading("c12", text="Typ")
|
|
tvTickets.heading("c13", text="Entwertet")
|
|
tvTickets.tag_configure('oddrow', background='white', font=("monospace", 10))
|
|
tvTickets.tag_configure('evenrow', background='lightblue', font=("monospace", 10))
|
|
#tvTicketsBar = ttk.Scrollbar(tvTickets, orient="vertical", command=tvTickets.yview)
|
|
#tvTicketsBar.pack(side='right', fill='x')
|
|
#tvTickets.configure(yscrollcommand = tvTicketsBar.set)
|
|
tvTickets.pack(fill='both')
|
|
|
|
def update():
|
|
getScans(tvScans)
|
|
getTickets(tvTickets)
|
|
global currentScan
|
|
currentScan=getLastScan() #bekomme den letzten, unbearbeiteten Scan
|
|
updateBarcodeLabel(barcodeLabel, currentScan)
|
|
totalValidatedLabel.config(text=getValidatedCount())
|
|
totalDuplicateCodeScansLabel.config(text=getScannedDuplicates())
|
|
window.after(2000, update)
|
|
|
|
update() # run first time
|
|
window.mainloop()
|