A PyQT GUI application for converting InfoLease report outputs into Excel files. Handles parsing and summarizing. Learns where files are meant to be store and compiles monthly and yearly summaries.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
InfoLeaseExtract/main.py

303 lines
14 KiB

from ILE_MainWindow import Ui_MainWindow
import sys
import os
import pandas as pd
import json
from PyQt5 import QtWidgets
from datetime import datetime as dt
import ILExtract as ilx
from logging import debug, DEBUG, basicConfig
with open("settings.json") as s:
settings = json.loads(s.read())
if settings["debug"]:
basicConfig(filename='debug.log', encoding='utf-8', level=DEBUG)
debug("\n\n\n########################### VERSION = 3.10 ###########################\n\n\n")
debug("Running main.py...")
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
"""
This is the main window that the users see. It's where they select the report file and report type.
"""
def __init__(self, *args, obj=None, **kwargs):
debug("MainWindow class init..")
super(MainWindow, self).__init__(*args, **kwargs)
self.setupUi(self)
# Allows the window to accept drag and drop files
# See dragEnterEvent an dropEvent
self.setAcceptDrops(True)
# Will hold the inputFile that is processed to create the ouput
self.inputFile = ""
# The location that the outputfile will be saved at
self.outputFile = ""
# Load the settings.json
with open("settings.json") as s:
self.settings = json.loads(s.read())
# Set the current report type to ACH as default
self.curReportType = "ach"
# Set the extract function to match
self.extract_function = ilx.ach
# Run the report_type change to check for saved input file paths
self.report_type_change()
#
# Actions
#
# Opens the file selection window and sets the input file accordingly
self.inputFileButton.clicked.connect(self.getfile)
# Opens file location dialog and sets the ouput accordingly
self.outputFileButton.clicked.connect(self.setOutput)
# Initates the processing of the selected file
self.processReportButton.clicked.connect(self.process_selection)
# Copies the last processed dataframe into the users clipboard
self.copyButton.clicked.connect(self.to_clipboard)
# Adjusts the extract function and file paths based on new report type in combo box
self.reportTypeCB.currentTextChanged.connect(self.report_type_change)
# Opens the folder specified in output file
self.openFolderButton.clicked.connect(lambda: self.openWithDefaultApp(self.outputFile.removesuffix(self.outputFile.split('/')[-1])))
# Opens the last processed excel document
self.openExcelButton.clicked.connect(lambda: self.openWithDefaultApp(self.outputFile))
# Sets the default text of the preview box
self.inputFilePreview.setText("Drag & Drop Input file here!")
def set_input(self, ifile):
"""
Triggerd by getFile() associated with the inputFile button
Reads the input file, sets the preview box to that excel doc and sets the
line edit and inputFile
Also sets the output file to a default structure. By default the output is put in the same
folder as the input file
! If the report type is minv_C we will save as .txt not .xlsx
Disables buttons since it is assumed this file has not yet been processed
- open excel button
- open folder button
- copy button
"""
debug(f"Executing set_input | input file: {ifile}")
with open(ifile, errors="replace") as inF:
txt = inF.read()
self.inputFilePreview.setText(txt)
self.inputFileLE.setText(ifile)
self.inputFile = ifile
# This gets the actual file name
inFileEnd = ifile.split('/')[-1]
# Takes just the root of the input file
outputRoot = self.inputFile.removesuffix(inFileEnd)
# Automatically sets output to be in the same file as input, with a naming scheme
# The report type selected in the combo box will dictate the naming
if self.reportTypeCB.currentText() == "Minv_C":
debug(f"MINV_C file selected ({self.inputFile})! Using .txt for output.")
self.outputFile = f"{outputRoot}{self.reportTypeCB.currentText()}_{dt.now().strftime('%Y%m%d_%H%M')}.txt"
else:
self.outputFile = f"{outputRoot}{self.reportTypeCB.currentText().replace('/','')}_{dt.now().strftime('%Y%m%d_%H%M')}.xlsx"
self.outputFileLE.setText(self.outputFile)
self.openExcelButton.setEnabled(False)
self.copyButton.setEnabled(False)
self.openFolderButton.setEnabled(True)
# Enables the process button
self.check_ready_to_process()
def getfile(self):
"""
Launches the file selection dialog then inits the set_input function
"""
debug("Launching getfile.")
inFile = QtWidgets.QFileDialog.getOpenFileName(self, 'Open file',directory=self.settings["defaultLocations"][self.curReportType])
# If the user does not select a file we just exit and do not change anything
if inFile[0] == '':
debug(f"User did not select an input file! {inFile}")
return 1
else:
self.set_input(inFile[0])
def dragEnterEvent(self, event):
"""
Allow users to drag file into the application
"""
debug(f"Drag and enter event triggered: {event}.")
if event.mimeData().hasUrls():
event.accept()
else:
event.ignore()
def dropEvent(self, event):
"""
Allow users to drag file into the application
"""
file = [u.toLocalFile() for u in event.mimeData().urls()][0]
debug(f"Drop event: {event} | {file}\n Running set_input")
self.set_input(file)
def setOutput(self):
"""
Launches dialog to select output location and naming.
"""
# This allows the user to change the automatic naming and location
outFile = QtWidgets.QFileDialog.getSaveFileName(self, "Output file name")
if outFile[0] == '': return ''
if self.reportTypeCB.currentText() == "Minv_C":
self.outputFileLE.setText(f"{outFile[0]}__{dt.now().strftime('%Y%m%d_%H_%M')}.txt")
else:
self.outputFileLE.setText(f"{outFile[0]}__{dt.now().strftime('%Y%m%d_%H_%M')}.xlsx")
debug(f"setOutput: {outFile}")
self.outputFile = f"{outFile[0]}__{dt.now().strftime('%Y%m%d_%H_%M')}.xlsx"
debug(f"setOutput | output file: {self.outputFile}")
self.check_ready_to_process()
def check_ready_to_process(self):
"""
Makes sure there is an input and output selected before allowing processing
Disables the button of outputFile is blank
"""
# Makes sure there is an input and output selected before allowing processing
rtp = True if ((self.inputFile != "") & (self.outputFile != "")) else False
if rtp :
self.processReportButton.setEnabled(True)
debug("check_ready_to_process: Process button enabled")
if self.outputFile == "":
self.openFolderButton.setEnabled(False)
debug("check_ready_to_process: Process button disabled")
def process_selection(self):
"""
Kicks off the actual processing of the selected files
"""
debug(f"""Beginning file processing:
\nInput File: {self.inputFile} | Extract Function: {self.extract_function} | Report Type: {self.curReportType}""")
self.inputFilePreview.setText("Processing file...")
# Use the ILReport class to process the file\
# need to set the input file, extract function to use, and output location
dataframe = ilx.ILReport(
location= self.inputFile,
extraction_function=self.extract_function,
output_location=self.outputFile,
).process()
debug(f"Process Selction | dataframe:\n{dataframe}")
# The text preview box can have trouble loading the larger dataframes so
# they are trimmed to 500 so that the users can see if anything got messed up
if dataframe.empty:
self.inputFilePreview.setText("Failed to create dataframe!")
return None
smallDF = dataframe.iloc[0:500,:]
self.inputFilePreview.setText(smallDF.to_html(index=False))
# Enable the excel button so users can open the file
self.openExcelButton.setEnabled(True)
# Enable the dopy button to copy this dataframe
self.copyButton.setEnabled(True)
# Checks if this input location is saved in settings.json.
# If there is no location for this report type it will be saved in the deafult
self.checked_for_saved()
# Allows user to open the output folder
self.openFolderButton.setEnabled(True)
def to_clipboard(self):
"""
Copies the file in output to the users clipboard
"""
df = pd.read_excel(self.outputFile)
df.to_clipboard(excel=True)
def openFolder(self):
outputRoot = self.outputFile.removesuffix(self.outputFile.split('/')[-1])
os.startfile(outputRoot)
def openWithDefaultApp(self, item):
"""
Opens the linked item with it's default application (excel)
"""
debug(f"openWithDefaultApp: {item}")
os.startfile(item)
def checked_for_saved(self):
"""
Checks if a report type already has a default location
If the default location is blank, the current inputFileLocation will be saved
"""
if self.settings["defaultLocations"][self.curReportType] == '':
self.settings["defaultLocations"][self.curReportType] = ('/').join(self.inputFile.split('/')[:-1])
debug(f"checked_for_saved: saved new deafult location | {self.curReportType} | {self.settings['defaultLocations'][self.curReportType]}")
with open('settings.json', 'w') as s:
# Save changes to the setting
json.dump(self.settings, s)
def report_type_change(self):
debug(f"Changing report type | Was: {self.curReportType} -> {self.reportTypeCB.currentText()}")
# Adjust the report type according to the combo box
# This will be used in settings the the extract function and determining file locations
# Disable the process report button since the input and output fields are now blank
self.processReportButton.setEnabled(False)
self.openExcelButton.setEnabled(False)
self.copyButton.setEnabled(False)
self.openFolderButton.setEnabled(False)
if self.reportTypeCB.currentText() == "ACH":
self.curReportType = "ach"
self.extract_function = ilx.ach
elif self.reportTypeCB.currentText() == "Disposition":
self.curReportType = "disp"
self.extract_function = ilx.disposition
elif self.reportTypeCB.currentText() == "Gain Loss":
self.curReportType = "gl"
self.extract_function = ilx.gainloss
elif self.reportTypeCB.currentText() == "Lock Box":
self.curReportType = "lb"
self.extract_function = ilx.lockbox
elif self.reportTypeCB.currentText() == "Minv_C":
self.curReportType = "minv"
self.extract_function = ilx.minv
elif self.reportTypeCB.currentText() == "Net Inv. Loans":
self.curReportType = "niv"
self.extract_function = ilx.net_invest_trial_balance
elif self.reportTypeCB.currentText() == "NI Renewal":
self.curReportType = "ren"
self.extract_function = ilx.renewal_net_invest_trial_balance
elif self.reportTypeCB.currentText() == "NIV After":
self.curReportType = "niv"
self.extract_function = ilx.net_invest_trial_balance
elif self.reportTypeCB.currentText() == "PBP / Epay":
self.curReportType = "pymt"
self.extract_function = ilx.payment_transactions
elif self.reportTypeCB.currentText() == "Unapplied":
self.curReportType = "uap"
self.extract_function = ilx.unapplied
elif self.reportTypeCB.currentText() == "Past Due":
self.curReportType = "pastdue"
self.extract_function = ilx.pastdue
elif self.reportTypeCB.currentText() == "VMCC":
self.curReportType = "pymt"
self.extract_function = ilx.payment_transactions
elif self.reportTypeCB.currentText() == "Wires":
self.curReportType = "pymt"
self.extract_function = ilx.payment_transactions
elif self.reportTypeCB.currentText() == "Returned Check":
self.curReportType = "pymt"
self.extract_function = ilx.payment_transactions
self.inputFile = ""
self.inputFileLE.setText(self.inputFile)
# Automatically sets output to be in the same file as input, with a naming scheme
# The report type selected in the combo box will dictate the naming
if self.curReportType == "minv":
self.outputFile = f"{self.reportTypeCB.currentText()}_{dt.now().strftime('%Y%m%d_%H%M')}.txt"
else:
self.outputFile = f"{self.reportTypeCB.currentText().replace('/','')}_{dt.now().strftime('%Y%m%d_%H%M')}.xlsx"
self.outputFileLE.setText(self.outputFile)
debug(f"report_type_change | inputFile: {self.inputFile}")
debug(f"report_type_change | outputFile: {self.outputFile}")
self.check_ready_to_process()
# Defines the app
app = QtWidgets.QApplication(sys.argv)
# Sets the style
app.setStyle("Fusion")
# Builds the main window
window = MainWindow()
window.setWindowTitle("IL Extract")
window.show()
# Starts the app
app.exec()