Initial Commit
This commit is contained in:
commit
87f1ec34b9
|
|
@ -0,0 +1,8 @@
|
|||
Department
|
||||
Audit
|
||||
IT
|
||||
Partners
|
||||
Payroll
|
||||
Reception
|
||||
Secretarys
|
||||
Tax
|
||||
|
|
|
@ -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)
|
||||
|
|
@ -0,0 +1 @@
|
|||
department,fname,sname,cardID
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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}")
|
||||
|
|
@ -0,0 +1 @@
|
|||
INFO:root:SIGN IN: Joe Bloggs : 15:35:48
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
department,fname,sname,cardID,signin
|
||||
,,,,
|
||||
|
|
|
@ -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
|
||||
Loading…
Reference in New Issue