Compare commits
7 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
e3625793da | 3 years ago |
|
|
cadf5f4b0b | 3 years ago |
|
|
5b03a6c7a9 | 3 years ago |
|
|
1bb12c722a | 3 years ago |
|
|
5caaf3d7ac | 3 years ago |
|
|
a3905d118e | 3 years ago |
|
|
5067678a8c | 3 years ago |
@ -1,10 +1,17 @@ |
|||||||
build/ |
build/ |
||||||
venv/ |
venv/ |
||||||
dist/ |
dist/ |
||||||
inputFiles/ |
InputFiles/ |
||||||
__pycache__/ |
__pycache__/ |
||||||
2023/ |
2023/ |
||||||
|
ilr_test/ |
||||||
|
|
||||||
|
*.lnk |
||||||
*.spec |
*.spec |
||||||
*.log |
*.log |
||||||
*.xlsx |
*.xlsx |
||||||
|
*.txt |
||||||
|
*.md |
||||||
|
|
||||||
|
!todo.md |
||||||
|
!requirements.txt |
||||||
@ -1 +0,0 @@ |
|||||||
pyinstaller -w --add-data "assets/extract.svg;." --add-data "assets/process.svg;." --add-data "assets/folder.svg;." --add-data "assets/copy.svg;." --add-data "settings.json;." -i assets/extract.ico -n "IL Extract" main.py |
|
||||||
@ -0,0 +1,61 @@ |
|||||||
|
from pathlib import Path |
||||||
|
from dataclasses import dataclass |
||||||
|
import os |
||||||
|
from datetime import datetime, timedelta, date |
||||||
|
|
||||||
|
import shutil as sh |
||||||
|
|
||||||
|
class Report: |
||||||
|
|
||||||
|
def __init__(self, sample_file: str, sample_folder: str): |
||||||
|
self.sample_file: Path = Path(sample_file) |
||||||
|
self.folder_name: Path = Path(sample_folder) |
||||||
|
|
||||||
|
def create_record(self, location: Path) -> Path|None: |
||||||
|
|
||||||
|
# Create the folder |
||||||
|
fp = Path(location, self.folder_name) |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def main(): |
||||||
|
|
||||||
|
folders = [ |
||||||
|
Report(r"InputFiles\2022.09.02_ACH_C", r"ACH"), |
||||||
|
Report(r"InputFiles\2022.09.02_PROGPAY_BER", r"CHECKS LIVE"), |
||||||
|
Report(r"InputFiles\2022.09.01_VMCC_BER", r"CREDIT CARDS"), |
||||||
|
Report(r"InputFiles\2022.09.02_DISPOSITION_PM_C", r"DISPOSITION REPORTING"), |
||||||
|
Report(r"InputFiles\2022.09.02_LOCKBOX_094_C", r"LOCKBOX"), |
||||||
|
Report(r"InputFiles\2022.09.02_PBP_EPAY_DPS_BER", r"PAY BY PHONE"), |
||||||
|
Report(r"InputFiles\2022.12.30_PBP_EPAY_RETURNS_BER", r"RETURN REPORTING"), |
||||||
|
Report(r"InputFiles\2022.09.01_PUB_WIRES_BER", r"WIRES"), |
||||||
|
] |
||||||
|
|
||||||
|
folder_date = date(2023,1,1) |
||||||
|
|
||||||
|
while folder_date < date(2024,1,1): |
||||||
|
|
||||||
|
year = folder_date.strftime("%Y") |
||||||
|
month = folder_date.strftime("%Y.%m") |
||||||
|
day = folder_date.strftime("%Y.%m.%d") |
||||||
|
|
||||||
|
date_path = Path(year, month, day) |
||||||
|
|
||||||
|
for rp in folders: |
||||||
|
|
||||||
|
|
||||||
|
# Create folder |
||||||
|
fold_p = Path(date_path, rp.folder_name) |
||||||
|
print(f"Creating filepath: {fold_p}") |
||||||
|
os.makedirs(fold_p, exist_ok=True) |
||||||
|
|
||||||
|
file_p = Path(fold_p, rp.sample_file.name) |
||||||
|
print(f"Cp {rp.sample_file} into {file_p}") |
||||||
|
|
||||||
|
sh.copyfile(rp.sample_file, file_p ) |
||||||
|
|
||||||
|
folder_date += timedelta(days=1) |
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
|
||||||
|
main() |
||||||
@ -1,118 +0,0 @@ |
|||||||
from os import system, getlogin |
|
||||||
import os |
|
||||||
from sys import exit |
|
||||||
from zipfile import ZipFile |
|
||||||
import win32com.client |
|
||||||
from glob import glob |
|
||||||
import re |
|
||||||
|
|
||||||
from itertools import cycle |
|
||||||
from shutil import get_terminal_size |
|
||||||
from threading import Thread |
|
||||||
from time import sleep |
|
||||||
|
|
||||||
def error_exit(exception_info: str): |
|
||||||
print(exception_info) |
|
||||||
input("\nPress enter/return to exit") |
|
||||||
exit(1) |
|
||||||
|
|
||||||
|
|
||||||
class NoMatchingFile(Exception): |
|
||||||
def __init__(self, search_file: str, found: list) -> None: |
|
||||||
super().__init__(f"File: {search_file} was not found: {found}") |
|
||||||
|
|
||||||
|
|
||||||
class Loader: |
|
||||||
def __init__(self, desc="Loading...", end="Done!", timeout=0.1): |
|
||||||
""" |
|
||||||
A loader-like context manager |
|
||||||
|
|
||||||
Args: |
|
||||||
desc (str, optional): The loader's description. Defaults to "Loading...". |
|
||||||
end (str, optional): Final print. Defaults to "Done!". |
|
||||||
timeout (float, optional): Sleep time between prints. Defaults to 0.1. |
|
||||||
""" |
|
||||||
self.desc = desc |
|
||||||
self.end = end |
|
||||||
self.timeout = timeout |
|
||||||
|
|
||||||
self._thread = Thread(target=self._animate, daemon=True) |
|
||||||
self.steps = ["|", "/", "-", "\\",] |
|
||||||
self.done = False |
|
||||||
|
|
||||||
def start(self): |
|
||||||
self._thread.start() |
|
||||||
return self |
|
||||||
|
|
||||||
def _animate(self): |
|
||||||
for c in cycle(self.steps): |
|
||||||
if self.done: |
|
||||||
break |
|
||||||
print(f"\r{self.desc} {c}", flush=True, end="") |
|
||||||
sleep(self.timeout) |
|
||||||
|
|
||||||
def __enter__(self): |
|
||||||
self.start() |
|
||||||
|
|
||||||
def stop(self): |
|
||||||
self.done = True |
|
||||||
cols = get_terminal_size((80, 20)).columns |
|
||||||
print("\r" + " " * cols, end="", flush=True) |
|
||||||
print(f"\r{self.end}", flush=True) |
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, tb): |
|
||||||
# handle exceptions with those variables ^ |
|
||||||
self.stop() |
|
||||||
|
|
||||||
ZIP_LOCATION = r"\\leafnow.com\public\Accounting Shared\ILE Apps" |
|
||||||
APP_FOLDER = r"InfoLeaseExtract" |
|
||||||
|
|
||||||
try: |
|
||||||
|
|
||||||
user = getlogin() |
|
||||||
install_folder = f"C:\\Users\\{user}\\AppData\\Local" |
|
||||||
backup_install_folder = f"C:\\Users\\{user}\\Documents\\" |
|
||||||
|
|
||||||
print(f"Initalizing InfoLease Extract Installer\n#######################################") |
|
||||||
# Find the newest version: |
|
||||||
latest_version = glob(f"{ZIP_LOCATION}\\LATEST*") |
|
||||||
if len(latest_version) == 0: |
|
||||||
# Create Custom exception |
|
||||||
raise NoMatchingFile(f"{ZIP_LOCATION}\\LATEST*", latest_version) |
|
||||||
|
|
||||||
latest_version: str = latest_version[0] |
|
||||||
version = re.search("\d+\.\d+", latest_version).group() |
|
||||||
print(f"Installing verion {version}...") |
|
||||||
|
|
||||||
with ZipFile(latest_version, 'r') as zipObj: |
|
||||||
try: |
|
||||||
with Loader("Setting up program files..."): |
|
||||||
zipObj.extractall(install_folder) |
|
||||||
except Exception as e: |
|
||||||
error_exit(f"Failed to extract file ({latest_version}) to '{install_folder}' :\n{e}") |
|
||||||
|
|
||||||
print("Creating Desktop shortcut...") |
|
||||||
try: |
|
||||||
desktop = f"C:\\Users\\{user}\\OneDrive - LEAF Commercial Capital\\Desktop" |
|
||||||
shell = win32com.client.Dispatch("WScript.Shell") |
|
||||||
shortcut = shell.CreateShortCut(os.path.join(desktop, "IL Extract v3.10.lnk"),) |
|
||||||
shortcut.Targetpath = f"{install_folder}\\IL Extract\\IL Extract.exe" |
|
||||||
shortcut.IconLocation = f"{install_folder}\\IL Extract\\assets\\extract.ico" |
|
||||||
shortcut.WorkingDirectory = f"{install_folder}\\IL Extract" |
|
||||||
shortcut.save() |
|
||||||
except: |
|
||||||
try: |
|
||||||
desktop = f"C:\\Users\\{user}\\Desktop" |
|
||||||
shell = win32com.client.Dispatch("WScript.Shell") |
|
||||||
shortcut = shell.CreateShortCut(os.path.join(desktop, "IL Extract v3.10.lnk"),) |
|
||||||
shortcut.Targetpath = f"{install_folder}\\IL Extract\\IL Extract.exe" |
|
||||||
shortcut.IconLocation = f"{install_folder}\\IL Extract\\assets\\extract.ico" |
|
||||||
shortcut.WorkingDirectory = f"{install_folder}\\IL Extract" |
|
||||||
shortcut.save() |
|
||||||
except Exception as e: |
|
||||||
error_exit(f"Failed to create shortcut. The application is still installed at:\n{install_folder}\\IL Extract.\nYou can manually create a shortcut if you would like.\n{e}") |
|
||||||
|
|
||||||
print(f"\nInstallation Completed Successfully!") |
|
||||||
input("\nPress Enter/Return to exit.") |
|
||||||
except Exception as e: |
|
||||||
error_exit(f"High level exception:\n{e}") |
|
||||||
@ -1 +1 @@ |
|||||||
{"debug": false, "consolidatedBasePath": "leafnow.com/shared/cashapps", "defaultLocations": {"ach": "", "disp": "", "gl": "", "lb": "", "minv": "", "niv": "", "ren": "", "pymt": "", "uap": "", "pastdue": ""}} |
{"debug": true, "consolidatedBasePath": ".", "defaultLocations": {"ach": "Z:/shared/Business Solutions/Griff/Code/InfoLeaseExtract/2023/2023.05/2023.05.24/ACH", "disp": "", "gl": "", "lb": "Z:/Business Solutions/Griff/Code/InfoLeaseExtract/InputFiles", "minv": "", "niv": "", "ren": "", "pymt": "Z:/Business Solutions/Griff/Code/InfoLeaseExtract/InputFiles", "uap": "", "pastdue": ""}} |
||||||
@ -0,0 +1,14 @@ |
|||||||
|
debug = true |
||||||
|
consolidatedBasePath = "" |
||||||
|
|
||||||
|
[defaultLocations] |
||||||
|
ach = "//leafnow.com/shared/Business Solutions/Griff/Code/InfoLeaseExtract/2023/2023.03/2023.03.01/ACH" |
||||||
|
disp = "" |
||||||
|
gl = "" |
||||||
|
lb = "//leafnow.com/shared/Business Solutions/Griff/Code/InfoLeaseExtract/2023/2023.03/2023.03.01/LOCKBOX" |
||||||
|
minv = "" |
||||||
|
niv = "" |
||||||
|
ren = "" |
||||||
|
pymt = "" |
||||||
|
uap = "" |
||||||
|
pastdue = "" |
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 477 B After Width: | Height: | Size: 477 B |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 819 B After Width: | Height: | Size: 819 B |
|
Before Width: | Height: | Size: 512 B After Width: | Height: | Size: 512 B |
|
Before Width: | Height: | Size: 568 B After Width: | Height: | Size: 568 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
@ -0,0 +1,12 @@ |
|||||||
|
{ |
||||||
|
"name": { |
||||||
|
"report": "", |
||||||
|
"excel": "" |
||||||
|
}, |
||||||
|
"relative_position": { |
||||||
|
"rows": 0, |
||||||
|
"col": 0 |
||||||
|
}, |
||||||
|
"length": 0, |
||||||
|
"data_type": "int" |
||||||
|
} |
||||||
@ -0,0 +1,184 @@ |
|||||||
|
from typing import TypeAlias, TypeVar |
||||||
|
from dataclasses import dataclass |
||||||
|
from pathlib import Path |
||||||
|
import pathlib as pl |
||||||
|
from abc import ABC, abstractmethod, abstractproperty |
||||||
|
from re import search, match, compile, Match, Pattern |
||||||
|
from enum import Enum |
||||||
|
|
||||||
|
ColumnIndex: TypeAlias = int |
||||||
|
Money: TypeAlias = float |
||||||
|
|
||||||
|
Numeric = TypeVar("Numeric", float, int) |
||||||
|
|
||||||
|
class Line(Enum): |
||||||
|
Header: str |
||||||
|
Data: str |
||||||
|
Erroneous: str |
||||||
|
Top: str |
||||||
|
Bottom: str |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass |
||||||
|
class RelativePosition: |
||||||
|
""" |
||||||
|
Coordinates for navigating from one point in a row to another |
||||||
|
""" |
||||||
|
rows: int |
||||||
|
col: ColumnIndex |
||||||
|
|
||||||
|
@dataclass |
||||||
|
class DataValue: |
||||||
|
|
||||||
|
position: RelativePosition |
||||||
|
length : int |
||||||
|
regex: Pattern |
||||||
|
dtype: type |
||||||
|
|
||||||
|
def correct_line(self, adj_lines_since_header: int) -> bool: |
||||||
|
""" |
||||||
|
""" |
||||||
|
return adj_lines_since_header % self.position.rows == 0 |
||||||
|
|
||||||
|
def _line_slice(self, line: Line.Data) -> str|None: |
||||||
|
""" |
||||||
|
Attempts to get the data from the line. |
||||||
|
Returns string in correct postion or None if out of range. |
||||||
|
""" |
||||||
|
try: |
||||||
|
start: int = self.position.col |
||||||
|
end: int = start + self.length |
||||||
|
line_slice: str = line[start:end] |
||||||
|
except IndexError: |
||||||
|
#TODO: Add logging |
||||||
|
line_slice = None |
||||||
|
finally: |
||||||
|
return line_slice |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def _to_float(number_str: str) -> float|None: |
||||||
|
try: |
||||||
|
f_value:float = float(number_str.replace(',','')) |
||||||
|
return f_value |
||||||
|
except: |
||||||
|
return None |
||||||
|
|
||||||
|
def extract(self, line: Line.Data) -> type|None: |
||||||
|
""" |
||||||
|
""" |
||||||
|
line_slice: str|None = self._line_slice(line) |
||||||
|
if isinstance(line_slice, None): |
||||||
|
return None |
||||||
|
|
||||||
|
value_match: Match|None = search(self.regex, line_slice) |
||||||
|
if isinstance(value_match, None): |
||||||
|
return None |
||||||
|
|
||||||
|
value_str: str = value_match.group() |
||||||
|
|
||||||
|
value_str.strip() |
||||||
|
if self.dtype == int or self.dtype == float: |
||||||
|
return self._to_float(value_str) |
||||||
|
#TODO datetime |
||||||
|
return value_str |
||||||
|
|
||||||
|
class DataSet: |
||||||
|
|
||||||
|
def __init__(self, config: dict) -> None: |
||||||
|
self.r_name = config["naming"]["report"] |
||||||
|
try: |
||||||
|
self.e_name = config["naming"]["excel"] |
||||||
|
except KeyError: |
||||||
|
self.e_name = self.r_name |
||||||
|
|
||||||
|
self.data_value: DataValue = DataValue( |
||||||
|
position = RelativePosition( |
||||||
|
rows= config["relative_position"]["rows"], |
||||||
|
col= config["relative_position"]["col"] |
||||||
|
), |
||||||
|
length = config["length"], |
||||||
|
dtype = config["data_type"], |
||||||
|
) |
||||||
|
|
||||||
|
def line_position(self, line: str) -> ColumnIndex|None: |
||||||
|
""" |
||||||
|
Searches a line for the report header for this dataset. |
||||||
|
|
||||||
|
Returns: |
||||||
|
- ColumnIndex(int) | None: The column index of the matches end position |
||||||
|
or None if no match was found |
||||||
|
""" |
||||||
|
header_match: Match|None = search(self.r_name, line) |
||||||
|
return header_match.end() if isinstance(header_match, Match) else None |
||||||
|
|
||||||
|
@dataclass |
||||||
|
class ReportConfig: |
||||||
|
|
||||||
|
file_extension: str |
||||||
|
name: str |
||||||
|
datasets: list[DataSet] |
||||||
|
data_line_regexes: list[Pattern] |
||||||
|
|
||||||
|
|
||||||
|
class ILReport(ABC): |
||||||
|
|
||||||
|
def __init__(self, file_path: Path, report_config: ReportConfig) -> None: |
||||||
|
self.in_file_path: Path = file_path |
||||||
|
self.line_gen = self._line_generator(file_path) |
||||||
|
|
||||||
|
self.config: ReportConfig = report_config |
||||||
|
self.name = report_config.name |
||||||
|
|
||||||
|
self.line_type_history: list[Line] = [] |
||||||
|
self.last_header_line: int|None = None |
||||||
|
|
||||||
|
self.data_dict: dict = { |
||||||
|
header.e_name: [] |
||||||
|
for header in self.config.datasets |
||||||
|
} |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def _line_generator(file_path: Path): |
||||||
|
with open(file_path, 'r') as in_file: |
||||||
|
line: str |
||||||
|
for line in in_file.readlines(): |
||||||
|
yield line |
||||||
|
|
||||||
|
def _add_line_history(self, line: Line, max_history: int = 10): |
||||||
|
self.line_type_history.append(line) |
||||||
|
while len(self.line_type_history) > max_history: |
||||||
|
self.line_type_history.pop(0) |
||||||
|
|
||||||
|
def _is_header_line(self, line: str) -> bool: |
||||||
|
""" |
||||||
|
Checks whether a report line has data headers. |
||||||
|
""" |
||||||
|
regex: Pattern |
||||||
|
for regex in self.config.data_line_regexes: |
||||||
|
if isinstance(search(regex,line), Match): |
||||||
|
return True |
||||||
|
return False |
||||||
|
|
||||||
|
@abstractmethod |
||||||
|
def _skip_line(self, line) -> bool: |
||||||
|
""" |
||||||
|
Tells whether we should skip this line |
||||||
|
""" |
||||||
|
|
||||||
|
@abstractmethod |
||||||
|
def _process_line(self): |
||||||
|
""" |
||||||
|
|
||||||
|
""" |
||||||
|
|
||||||
|
@abstractmethod |
||||||
|
def _process_dataline(self, dataline: Line.Data): |
||||||
|
""" |
||||||
|
""" |
||||||
|
|
||||||
|
# Search the row for a data set name, or list of data set names |
||||||
|
# extract all the data until the next row |
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
datasets = [] |
||||||
@ -1,3 +1,6 @@ |
|||||||
|
""" |
||||||
|
The user interface set up for the main window of the application |
||||||
|
""" |
||||||
# -*- coding: utf-8 -*- |
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
# Form implementation generated from reading ui file 'ILE_MainWindow.ui' |
# Form implementation generated from reading ui file 'ILE_MainWindow.ui' |
||||||
@ -0,0 +1,37 @@ |
|||||||
|
# Work List |
||||||
|
|
||||||
|
## Priority |
||||||
|
- [ ] Bring back in consolidated reports |
||||||
|
- [X] ACH |
||||||
|
- [X] CHECKS_LIVE |
||||||
|
- [X] CREDIT |
||||||
|
- [X] LOCKBOX |
||||||
|
- [X] PAY BY PHONE |
||||||
|
- [X] WIRES |
||||||
|
- [X] RETURNS ACH |
||||||
|
- [X] RETURNS Portal *(new addition)* |
||||||
|
|
||||||
|
- [X] Adjust pyinstaller spec for new file structure |
||||||
|
|
||||||
|
- [ ] Function to recap year |
||||||
|
- [ ] Fix Logging |
||||||
|
|
||||||
|
## Feature Goals |
||||||
|
- [ ] Year Walkthrough report |
||||||
|
- [ ] 'In Progress' notification/spinner |
||||||
|
- Speed up ACH/All |
||||||
|
|
||||||
|
Generate monthly consolidated reports for each month in a year |
||||||
|
- Must generate IL Extract report where nesseary |
||||||
|
|
||||||
|
- [ ] Users can add/create new reports |
||||||
|
- This would be very complicated |
||||||
|
|
||||||
|
## Code Improvement |
||||||
|
- [ ] Rework IL Report as an ABC and report types as sub-classes |
||||||
|
- [ ] Rework config set up |
||||||
|
- [ ] Simpify & standardize row/data parsing |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
--- |
||||||