Initial Commit

This commit is contained in:
jnisbet 2024-04-10 10:37:17 +01:00
commit 87f1ec34b9
10 changed files with 528 additions and 0 deletions

8
departments.csv Normal file
View File

@ -0,0 +1,8 @@
Department
Audit
IT
Partners
Payroll
Reception
Secretarys
Tax
1 Department
2 Audit
3 IT
4 Partners
5 Payroll
6 Reception
7 Secretarys
8 Tax

125
employee_manifest.py Normal file
View File

@ -0,0 +1,125 @@
from tkinter import *
from tkinter import ttk
import pandas as pd
from lib import treerow_add, csvrow_add, treerow_del, csvrow_del, row_tracker
from tkinter import messagebox
class EmployeeManifest(Frame):
def __init__(self, parent, controller):
Frame.__init__(self, parent)
self.employeereg_columns = ['department','fname','sname','cardID']
#Data frames for list of departments and database of users
self.employee_df = pd.read_csv('employeeregister.csv', converters={'cardID': str})
self.department_df = pd.read_csv('departments.csv')
self.row_tracker = row_tracker(self.department_df, 0)
self.input_frame = Frame(self)
self.input_frame.grid(row=0, column=0, sticky='w')
self.firstname_label = Label(self.input_frame, text='First Name')
self.firstname_label.grid(row=0, column=0, padx=10, sticky="w")
self.firstname_text = Entry(self.input_frame)
self.firstname_text.grid(row=0, column=1, pady=5)
self.firstname_text.focus()
self.surname_label = Label(self.input_frame, text='Surname')
self.surname_label.grid(row=1, column=0, padx=10, sticky="w")
self.surname_text = Entry(self.input_frame)
self.surname_text.grid(row=1, column=1, pady=5)
self.card_label = Label(self.input_frame, text='Scan Card')
self.card_label.grid(row=2, column=0, padx=10, sticky="w")
self.card_text = Entry(self.input_frame)
self.card_text.grid(row=2, column=1, pady=5)
self.enter_btn = Button(self.input_frame, text='Enter Employee Info', command= lambda: self.write_line_csv(self.get_inputs()))
self.enter_btn.grid(row=3, column=0, pady=5, padx=10)
self.del_row_btn = Button(self.input_frame, text='Delete Employee(s)', command=self.delete_employees)
self.del_row_btn.grid(row=3, column=1, padx=10)
#create and store options for the department option menu
self.department_options = []
for rownum, row in self.department_df.iterrows():
self.department_options.append(row.Department)
#Create department option menu
self.value_inside = StringVar(self.input_frame)
self.value_inside.set("")
self.options_label = Label(self.input_frame, text='Department')
self.options_label.grid(row=0, column=2, padx=10)
self.department_options = OptionMenu(self.input_frame, self.value_inside, *self.department_options)
self.department_options.config(width=14)
self.department_options.grid(row=0, column=3, padx=(0, 10))
self.employee_register_tree = ttk.Treeview(self)
self.employee_register_tree.grid(column=0, row=4, padx=(10, 0), sticky="we")
self.employee_register_tree['columns'] = ('First Name', 'Surname', 'Card Code')
self.employee_register_tree.column('#0', width=120)
self.employee_register_tree.column('First Name', anchor=CENTER, width=110)
self.employee_register_tree.column('Surname', anchor=CENTER, width=110)
self.employee_register_tree.column('Card Code', anchor=CENTER, width=110)
#Define headings for each column
self.employee_register_tree.heading('#0', text='Department')
self.employee_register_tree.heading('First Name', text='First Name')
self.employee_register_tree.heading('Surname', text='Surname')
self.employee_register_tree.heading('Card Code', text='Card Code')
self.tree_scrollbar = Scrollbar(self, orient="vertical", command=self.employee_register_tree.yview)
self.employee_register_tree.configure(yscrollcommand=self.tree_scrollbar.set)
self.tree_scrollbar.grid(column=1,row=4, sticky='ns', pady=(0, 10))
for rownum, row in self.employee_df.iterrows():
treerow_add(self.row_tracker, self.employee_register_tree, row)
def delete_employees(self):
employees = self.employee_register_tree.selection()
#Prevent deletion of department parent row
for employee in employees:
if 'D' in employee:
return
delete_prompt = messagebox.askokcancel('Delete Users', 'Are you sure you wish to delete user(s)')
if delete_prompt == False:
return
#Delete user from tree and CSV
self.employee_df = csvrow_del('employeeregister.csv', self.employee_df, employees)
treerow_del(self.row_tracker, self.employee_register_tree, employees)
#Clear inputs of all fields
def clear_inputs(self, input_arr):
self.value_inside.set("")
for input in input_arr:
input.delete('0', END)
self.firstname_text.focus()
#Return array of inputs in all fields
def get_inputs(self):
inputs = [self.value_inside.get(), self.firstname_text.get(), self.surname_text.get(), self.card_text.get()]
return inputs
def write_line_csv(self, inputs):
#Field validation
for input in inputs:
if input == '':
messagebox.showerror('Validation Error', 'Please make sure all fields are completed')
return
for x in range(len(self.employee_df)):
if self.employee_df.iloc[x]['cardID'] == inputs[3]:
messagebox.showerror('Validation Error', 'Card ID must be unique for each employee')
return
self.clear_inputs([self.firstname_text, self.surname_text, self.card_text])
#Add user to tree and csv
treerow_add(self.row_tracker, self.employee_register_tree, inputs)
self.employee_df = csvrow_add('employeeregister.csv', self.employee_df, self.employeereg_columns, inputs)

1
employeeregister.csv Normal file
View File

@ -0,0 +1 @@
department,fname,sname,cardID
1 department fname sname cardID

16
home.py Normal file
View File

@ -0,0 +1,16 @@
from tkinter import *
from tkinter import ttk
class Home(Frame):
def __init__(self, parent, controller):
super().__init__(parent)
btn_frame = Frame(self)
btn_frame.pack()
employee_management_btn = Button(btn_frame, text='Employess Management', command=lambda: controller.frame_update('employee_manifest'))
employee_management_btn.pack(pady = (0,20))
signin_btn = Button(btn_frame, text='Sign in Register', command=lambda: controller.frame_update('sign_in'))
signin_btn.pack()

86
index.py Normal file
View File

@ -0,0 +1,86 @@
from tkinter import *
from tkinter import ttk
import pandas as pd
from employee_manifest import EmployeeManifest
from home import Home
from sign_in import SignIn
import sched
import time
import os
from datetime import datetime, date , timedelta
from threading import Thread
from queue import Queue
#Special mention to James Haywood for imaging the PI $$ <3 <3!
class Main(Tk):
def __init__(self):
Tk.__init__(self)
if os.name == 'nt':
self.geometry('520x450')
else:
self.geometry('720x450')
self.winfo_toplevel().title("Sign in App")
self.main_frame = Frame(self)
self.main_frame.pack(side='top', fill=BOTH, expand=1, pady=10)
#Thread safe queue and event to check queue for scheduled sign out at midnight
#Required because scheduler is run in seperate thread which causes tkinter to break without
self.sched_queue = Queue()
self.bind('<<CheckQueue>>', self.run_signout)
#List of GUI's used for mounting on button click
class FramesList():
home = Home
employee_manifest = EmployeeManifest
sign_in = SignIn
self.frames = FramesList()
#Mount home frame when class envoked (program started)
self.current_frame = self.frames.home(self.main_frame, self)
self.current_frame.place(anchor="c", relx=.5, rely=.5)
self.home_btn = Button(text = 'Home', command=lambda: Main.frame_update(self, 'home'))
self.home_btn.place(anchor='w', x=5, y=20)
self.sched_thread = Thread(target=self.run_sched, daemon=True)
self.sched_thread.start()
#Destroy the current GUI frame and mount a requested GUI
def frame_update(self, requested_frame):
self.current_frame.destroy()
next_frame = getattr(self.frames, requested_frame)
self.current_frame = next_frame(self.main_frame, self)
self.current_frame.place(anchor="c", relx=.5, rely=.5)
#Add function to the thread safe queue and then generate an event that causes the queue to be checked
def update_queue(event, self):
self.sched_queue.put(self.clear_all_signin)
self.event_generate('<<CheckQueue>>')
#When CheckQueue event is fired this function is run
#Gets function from queue and then runs it
def run_signout(self, event):
msg = self.sched_queue.get()
msg()
#Clears sign ins (scheduled to run at the start of next day) by just destroying and re-mounting the sign in frame
def clear_all_signin(self):
print('clear sign in executed')
if type(app.current_frame).__name__ == 'SignIn':
print('p0ng')
self.frame_update('sign_in')
self.run_sched()
#Rune scheduled task at 12am each day
def run_sched(self):
scheduler = sched.scheduler(time.time, time.sleep)
tomorrows_date = date.today() + timedelta(1)
#todays_date = date.today()
scheduled_time = datetime(tomorrows_date.year, tomorrows_date.month, tomorrows_date.day, 00, 00, 00)
scheduler.enterabs(scheduled_time.timestamp(), 1, self.clear_all_signin)
scheduler.run()
app = Main()
app.mainloop()

65
lib.py Normal file
View File

@ -0,0 +1,65 @@
from tkinter import *
from tkinter import ttk
import pandas as pd
#Uses a counter to ensure unique iid for each row
#keeps track of parent row iids
#returns parent iids incrementing the counter each time
def row_tracker(department_frame, iid_count):
class Row_Tracker:
def __init__(self, iid_count):
self.iid_count = iid_count
def departiid(self, department):
self.iid_count += 1
setattr(self, department, 'D' + str(self.iid_count))
return getattr(self, department)
row_tracker = Row_Tracker(iid_count)
for rownum, row in department_frame.iterrows():
setattr(row_tracker, row['Department'], False)
return row_tracker
#add a row to the tree
def treerow_add(row_tracker, tree, inputs):
row_iid = inputs[3]
department = inputs[0]
department_iid = getattr(row_tracker, department)
if not getattr(row_tracker, department):
department_iid = row_tracker.departiid(department)
tree.insert(parent='', index='end', iid=department_iid, text=department, values=('', '', ''))
tree.insert(parent=department_iid, index='end', iid=row_iid, text='', values=(inputs[1], inputs[2], row_iid))
#add a row to the csv
# return frame because variable for that frame needs to be updated
def csvrow_add(file_path, frame, columns, inputs):
df_addition = {}
for i, column in enumerate(columns):
df_addition[column] = inputs[i]
frame = frame._append(df_addition, ignore_index = True)
frame.to_csv(file_path, index=False)
return frame
#Delete employess from the tree and csv
#If parent row has no remaining children, delete it row from the tree and reset tracking for it's iid
def treerow_del(row_tracker, tree, iids):
for iid in iids:
parent_row_iid = tree.parent(iid)
tree.delete(iid)
if not tree.get_children(parent_row_iid):
parent_row = tree.item(parent_row_iid)
setattr(row_tracker, parent_row['text'], False)
tree.delete(parent_row_iid)
#Delete employee from data frame and csv
def csvrow_del(file_path, frame, iids):
for iid in iids:
frame = frame.drop(frame[frame['cardID'] == iid].index)
frame.to_csv(file_path, index=False)
return frame

156
sign_in.py Normal file
View File

@ -0,0 +1,156 @@
from tkinter import *
from tkinter import ttk
import pandas as pd
import os
import re
import logging
from datetime import datetime, timedelta
from lib import row_tracker, treerow_add, csvrow_add, csvrow_del, treerow_del
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
class SignIn(Frame):
def __init__(self, parent, controller):
Frame.__init__(self, parent)
print('Signin refresh')
self.columns_signin = ['department', 'fname', 'sname', 'cardID', 'signin']
self.current_date = str(datetime.now().date())
#File path generation for logs and csv of sign ins
self.todays_signins_path = 'sign_ins/signin-'+ self.current_date + '.csv'
self.todays_log_path = 'sign_in_logs/' + self.current_date + '.log'
self.recent_signin = [False, timedelta(0)]
logging.basicConfig(level=logging.INFO, filename=self.todays_log_path, filemode="a")
#Data frames for list of departments and database of users
self.employee_df = pd.read_csv('employeeregister.csv', converters={'cardID': str})
self.department_df = pd.read_csv('departments.csv', converters={'cardID': str})
self.row_trackerobj = row_tracker(self.department_df, 0)
self.signin_df = self.load_signin_df()
self.input_frame = Frame(self)
self.input_frame.grid(column=0, row=0, columnspan=2, padx=10, pady=(0,30))
self.scan_label = Label(self.input_frame, text='Scan Card')
self.scan_label.grid(column=0, row=0, pady=10)
self.border_color = Frame(self.input_frame)
self.scan_text = Entry(self.border_color)
self.scan_text.focus()
self.scan_text.bind("<FocusIn>", self.focus_color)
self.scan_text.bind("<FocusOut>", self.unfocus_color)
self.scan_text.bind('<Return>', self.register_scan)
self.scan_text.pack(padx=3, pady=3)
self.border_color.grid(column=0, row=1)
self.tree_frame = Frame(self)
self.tree_frame.grid(column=0, row=2)
self.fire_register_tree = ttk.Treeview(self.tree_frame)
self.fire_register_tree.grid(column=0, row=0, padx=10)
self.fire_register_tree['columns'] = ('First Name', 'Surname', 'Sign in Time')
self.fire_register_tree.column('#0', width=120)
self.fire_register_tree.column('First Name', anchor=CENTER, width=110)
self.fire_register_tree.column('Surname', anchor=CENTER, width=110)
self.fire_register_tree.column('Sign in Time', anchor=CENTER, width=110)
#Define headings for each column
self.fire_register_tree.heading('#0', text='Department')
self.fire_register_tree.heading('First Name', text='First Name')
self.fire_register_tree.heading('Surname', text='Surname')
self.fire_register_tree.heading('Sign in Time', text='Sign in Time')
self.pdf_btn = Button(self, text='Signin Report', command=self.generate_pdf)
self.pdf_btn.grid(column=0, row=3, pady=(10,0))
#Build tree rows for existing sign ins (needed if program restarted after users have already signed in for the day)
for rownum, row in self.signin_df.iterrows():
treerow_add(self.row_trackerobj, self.fire_register_tree, row)
def load_signin_df(self):
sign_ins = os.listdir('sign_ins')
for file in sign_ins:
if re.findall(self.current_date, file):
return pd.read_csv(self.todays_signins_path, converters={'cardID': str, 'iid': str})
df = pd.DataFrame(columns=self.columns_signin)
df.to_csv(self.todays_signins_path, index=False)
return df
#Clear input of text box
def input_clear(self):
self.scan_text.delete('0', END)
self.scan_text.focus()
#change border colour of text box when focused/unfocused
def focus_color(self, event):
self.border_color.configure(background='green')
def unfocus_color(self, event):
self.border_color.configure(background='red')
def register_scan(self, event):
card_code = self.scan_text.get()
signin_datetime = datetime.now()
signin_time = signin_datetime.strftime("%H:%M:%S")
signin_datetime_future = signin_datetime + timedelta(seconds = 10)
self.input_clear()
# If employee does not exist exit
if not self.employee_df.loc[self.employee_df['cardID'] == card_code].values.size:
return
#Prevent double tap of card
if card_code == self.recent_signin[0] and self.recent_signin[1] > signin_datetime:
return
self.recent_signin = [card_code, signin_datetime_future]
code_linked_employee = self.employee_df.loc[self.employee_df['cardID'] == card_code].values[0].tolist()
#If employee has already signed in sign out. remove from tree, csv and create a log
if self.signin_df.loc[self.signin_df['cardID'] == card_code].values.size:
self.signin_df = csvrow_del(self.todays_signins_path, self.signin_df, [card_code])
treerow_del( self.row_trackerobj, self.fire_register_tree, [card_code])
logging.info('SIGN OUT: ' + code_linked_employee[1] + ' ' + code_linked_employee[2] + ' : ' + signin_time)
return
code_linked_employee.append(signin_time)
tree_row_toadd = [code_linked_employee[0], code_linked_employee[1], code_linked_employee[2], code_linked_employee[3]]
#Add employee to tree and sign in csv, also create log
treerow_add( self.row_trackerobj, self.fire_register_tree, tree_row_toadd)
self.signin_df = csvrow_add(self.todays_signins_path, self.signin_df, self.columns_signin, code_linked_employee)
logging.info('SIGN IN: ' + code_linked_employee[1] + ' ' + code_linked_employee[2] + ' : ' + signin_time)
#Populate PDF and send to default printer
def generate_pdf(self):
w, h = A4
current_row = 150
register_filename = "signinregister.pdf"
c = canvas.Canvas(register_filename, pagesize=A4)
c.drawString(30, h - 100, "Department")
c.drawString(150, h - 100, "First Name")
c.drawString(270, h - 100, "Surname")
c.drawString(390, h - 100, "Sign in Time")
for rownnum, row in self.signin_df.iterrows():
current_row += 20
c.drawString(30, h - current_row, row[0])
c.drawString(150, h - current_row, row[1])
c.drawString(270, h - current_row, row[2])
c.drawString(390, h - current_row, row[4])
c.showPage()
c.save()
if os.name == 'nt':
os.startfile(register_filename, "print")
else:
os.system(f"lp {register_filename}")

View File

@ -0,0 +1 @@
INFO:root:SIGN IN: Joe Bloggs : 15:35:48

View File

@ -0,0 +1,2 @@
department,fname,sname,cardID,signin
,,,,
1 department fname sname cardID signin
2

68
signinregister.pdf Normal file
View File

@ -0,0 +1,68 @@
%PDF-1.3
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/Contents 7 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 6 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
4 0 obj
<<
/PageMode /UseNone /Pages 6 0 R /Type /Catalog
>>
endobj
5 0 obj
<<
/Author (anonymous) /CreationDate (D:20240402090541+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240402090541+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (unspecified) /Title (untitled) /Trapped /False
>>
endobj
6 0 obj
<<
/Count 1 /Kids [ 3 0 R ] /Type /Pages
>>
endobj
7 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 202
>>
stream
Gas3-_$YcZ'SYMZ:N;]N_RqqlkYb#$U.t@c,]oDe`__$^,[0EY`5#Xeh[bJR9ohL4Aq6&LXU#Vn>EB*opl#:[dm&7p&g7o&8sr>V_.qjil[C[M*S942GiO$@SY4F00]H!"k'%AL:2kXSeuP"ieQQ*h$eF/[_Ch;m8+nTC^f0u?nj8Y':8u(EG_%:qP-bd@`Y#iaE'RX]~>endstream
endobj
xref
0 8
0000000000 65535 f
0000000073 00000 n
0000000104 00000 n
0000000211 00000 n
0000000414 00000 n
0000000482 00000 n
0000000778 00000 n
0000000837 00000 n
trailer
<<
/ID
[<325c24b56bce97acfa22c1fe23eb8ce5><325c24b56bce97acfa22c1fe23eb8ce5>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 5 0 R
/Root 4 0 R
/Size 8
>>
startxref
1129
%%EOF