updating the scripts
This commit is contained in:
97
cleaners/clean-halifax-actual.py
Normal file
97
cleaners/clean-halifax-actual.py
Normal file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import csv
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
RAW_DIR = Path("~/master-folder/attachments/bank-statements/halifax/raw").expanduser()
|
||||
CLEANED_DIR = Path("~/master-folder/attachments/bank-statements/halifax/cleaned").expanduser()
|
||||
|
||||
|
||||
def convert(in_path: Path, out_path: Path):
|
||||
with open(in_path, newline="", encoding="utf-8-sig") as f_in, \
|
||||
open(out_path, "w", newline="", encoding="utf-8") as f_out:
|
||||
|
||||
reader = csv.DictReader(f_in)
|
||||
|
||||
fieldnames = ["Date", "Payee", "Description", "Amount"]
|
||||
writer = csv.DictWriter(f_out, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
|
||||
for row in reader:
|
||||
if not row:
|
||||
continue
|
||||
|
||||
date = (row.get("Transaction Date") or "").strip()
|
||||
if not date:
|
||||
continue
|
||||
|
||||
desc = (row.get("Transaction Description") or "").strip()
|
||||
payee = desc or (row.get("Transaction Type") or "").strip()
|
||||
|
||||
debit_str = (row.get("Debit Amount") or "").replace(",", "").strip()
|
||||
credit_str = (row.get("Credit Amount") or "").replace(",", "").strip()
|
||||
|
||||
amount = None
|
||||
|
||||
if debit_str:
|
||||
try:
|
||||
amount = -Decimal(debit_str)
|
||||
except InvalidOperation:
|
||||
continue
|
||||
elif credit_str:
|
||||
try:
|
||||
amount = Decimal(credit_str)
|
||||
except InvalidOperation:
|
||||
continue
|
||||
|
||||
if amount is None:
|
||||
continue
|
||||
|
||||
writer.writerow({
|
||||
"Date": date,
|
||||
"Payee": payee,
|
||||
"Description": desc,
|
||||
"Amount": f"{amount:.2f}",
|
||||
})
|
||||
|
||||
|
||||
def select_file(files: list[Path]) -> Path:
|
||||
print("\nSelect a Halifax CSV to clean:\n")
|
||||
for i, f in enumerate(files, start=1):
|
||||
print(f"[{i}] {f.name}")
|
||||
|
||||
try:
|
||||
choice = int(input("\nEnter number: ").strip())
|
||||
return files[choice - 1]
|
||||
except (ValueError, IndexError):
|
||||
print("Invalid selection.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
if not RAW_DIR.exists():
|
||||
print(f"Raw directory does not exist: {RAW_DIR}")
|
||||
sys.exit(1)
|
||||
|
||||
CLEANED_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
csv_files = sorted(RAW_DIR.glob("*.csv"))
|
||||
|
||||
if not csv_files:
|
||||
print("No CSV files found in raw directory.")
|
||||
sys.exit(0)
|
||||
|
||||
in_path = select_file(csv_files)
|
||||
|
||||
out_name = f"{in_path.stem}-cleaned.csv"
|
||||
out_path = CLEANED_DIR / out_name
|
||||
|
||||
convert(in_path, out_path)
|
||||
print(f"\n✔ Written cleaned file to:\n{out_path}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
84
cleaners/clean-halifax-actual.py~
Normal file
84
cleaners/clean-halifax-actual.py~
Normal file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import csv
|
||||
from decimal import Decimal, InvalidOperation
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def convert(in_path, out_path):
|
||||
# Read Halifax CSV and write a cleaned CSV for Actual
|
||||
with open(in_path, newline="", encoding="utf-8-sig") as f_in, \
|
||||
open(out_path, "w", newline="", encoding="utf-8") as f_out:
|
||||
|
||||
reader = csv.DictReader(f_in)
|
||||
|
||||
# Output columns tailored for Actual’s CSV import
|
||||
fieldnames = ["Date", "Payee", "Description", "Amount"]
|
||||
writer = csv.DictWriter(f_out, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
|
||||
for row in reader:
|
||||
if not row:
|
||||
continue
|
||||
|
||||
# Date in Halifax export (dd/mm/yyyy)
|
||||
date = (row.get("Transaction Date") or "").strip()
|
||||
if not date:
|
||||
# Skip lines without a date
|
||||
continue
|
||||
|
||||
desc = (row.get("Transaction Description") or "").strip()
|
||||
payee = desc or (row.get("Transaction Type") or "").strip()
|
||||
|
||||
# Halifax has separate Debit / Credit columns
|
||||
debit_str = (row.get("Debit Amount") or "").replace(",", "").strip()
|
||||
credit_str = (row.get("Credit Amount") or "").replace(",", "").strip()
|
||||
|
||||
amount = None
|
||||
|
||||
# Debits (money going out) → negative
|
||||
if debit_str:
|
||||
try:
|
||||
amount = -Decimal(debit_str)
|
||||
except InvalidOperation:
|
||||
amount = None
|
||||
|
||||
# Credits (money coming in) → positive
|
||||
elif credit_str:
|
||||
try:
|
||||
amount = Decimal(credit_str)
|
||||
except InvalidOperation:
|
||||
amount = None
|
||||
|
||||
# If we still don't have an amount, skip the row
|
||||
if amount is None:
|
||||
continue
|
||||
|
||||
writer.writerow({
|
||||
"Date": date,
|
||||
"Payee": payee,
|
||||
"Description": desc,
|
||||
"Amount": f"{amount:.2f}",
|
||||
})
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python clean_halifax_actual.py input.csv [output.csv]")
|
||||
sys.exit(1)
|
||||
|
||||
in_path = Path(sys.argv[1])
|
||||
|
||||
if len(sys.argv) >= 3:
|
||||
out_path = Path(sys.argv[2])
|
||||
else:
|
||||
# Default to inputname_actual.csv
|
||||
out_path = in_path.with_name(in_path.stem + "_actual.csv")
|
||||
|
||||
convert(in_path, out_path)
|
||||
print(f"Written cleaned file to {out_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
132
cleaners/nationwide-cleaner.py
Normal file
132
cleaners/nationwide-cleaner.py
Normal file
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import csv
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import sys
|
||||
|
||||
|
||||
RAW_DIR = Path("/home/zaine/master-folder/attachments/bank-statements/nationwide/raw")
|
||||
CLEANED_DIR = Path("/home/zaine/master-folder/attachments/bank-statements/nationwide/cleaned")
|
||||
|
||||
|
||||
def clean_money(value: str) -> Decimal | None:
|
||||
if not value:
|
||||
return None
|
||||
|
||||
value = (
|
||||
value.replace("£", "")
|
||||
.replace(",", "")
|
||||
.strip()
|
||||
)
|
||||
|
||||
if not value:
|
||||
return None
|
||||
|
||||
try:
|
||||
return Decimal(value)
|
||||
except InvalidOperation:
|
||||
return None
|
||||
|
||||
|
||||
def parse_date(value: str) -> str | None:
|
||||
"""
|
||||
Convert '01 Dec 2025' → '12/01/2025'
|
||||
"""
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return None
|
||||
|
||||
try:
|
||||
dt = datetime.strptime(value, "%d %b %Y")
|
||||
return dt.strftime("%m/%d/%Y")
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def convert(in_path: Path, out_path: Path):
|
||||
with open(in_path, newline="", encoding="latin-1") as f_in, \
|
||||
open(out_path, "w", newline="", encoding="utf-8") as f_out:
|
||||
|
||||
reader = csv.reader(f_in)
|
||||
|
||||
# Skip metadata lines until header
|
||||
for row in reader:
|
||||
if row and row[0] == "Date":
|
||||
header = row
|
||||
break
|
||||
else:
|
||||
raise RuntimeError("Could not find Nationwide CSV header row")
|
||||
|
||||
dict_reader = csv.DictReader(f_in, fieldnames=header)
|
||||
|
||||
fieldnames = ["Date", "Payee", "Description", "Amount"]
|
||||
writer = csv.DictWriter(f_out, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
|
||||
for row in dict_reader:
|
||||
raw_date = row.get("Date") or ""
|
||||
date = parse_date(raw_date)
|
||||
if not date:
|
||||
continue
|
||||
|
||||
desc = (row.get("Description") or "").strip()
|
||||
payee = desc
|
||||
|
||||
paid_out = clean_money(row.get("Paid out", ""))
|
||||
paid_in = clean_money(row.get("Paid in", ""))
|
||||
|
||||
if paid_out is not None:
|
||||
amount = -paid_out
|
||||
elif paid_in is not None:
|
||||
amount = paid_in
|
||||
else:
|
||||
continue
|
||||
|
||||
writer.writerow({
|
||||
"Date": date,
|
||||
"Payee": payee,
|
||||
"Description": desc,
|
||||
"Amount": f"{amount:.2f}",
|
||||
})
|
||||
|
||||
|
||||
def select_file(files: list[Path]) -> Path:
|
||||
print("\nSelect a Nationwide CSV to clean:\n")
|
||||
for i, f in enumerate(files, start=1):
|
||||
print(f"[{i}] {f.name}")
|
||||
|
||||
try:
|
||||
choice = int(input("\nEnter number: ").strip())
|
||||
return files[choice - 1]
|
||||
except (ValueError, IndexError):
|
||||
print("Invalid selection.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
if not RAW_DIR.exists():
|
||||
print(f"Raw directory does not exist: {RAW_DIR}")
|
||||
sys.exit(1)
|
||||
|
||||
CLEANED_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
csv_files = sorted(RAW_DIR.glob("*.csv"))
|
||||
|
||||
if not csv_files:
|
||||
print("No CSV files found in raw directory.")
|
||||
sys.exit(0)
|
||||
|
||||
in_path = select_file(csv_files)
|
||||
|
||||
out_name = f"{in_path.stem}-cleaned.csv"
|
||||
out_path = CLEANED_DIR / out_name
|
||||
|
||||
convert(in_path, out_path)
|
||||
|
||||
print(f"\n✔ Written cleaned file to:\n{out_path}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
104
cleaners/nationwide-cleaner.py~
Normal file
104
cleaners/nationwide-cleaner.py~
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import csv
|
||||
from decimal import Decimal, InvalidOperation
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def clean_money(value: str) -> Decimal | None:
|
||||
if not value:
|
||||
return None
|
||||
|
||||
value = (
|
||||
value.replace("£", "")
|
||||
.replace(",", "")
|
||||
.strip()
|
||||
)
|
||||
|
||||
if not value:
|
||||
return None
|
||||
|
||||
try:
|
||||
return Decimal(value)
|
||||
except InvalidOperation:
|
||||
return None
|
||||
|
||||
def parse_date(value: str) -> str | None:
|
||||
"""
|
||||
Convert '01 Dec 2025' → '12/01/2025'
|
||||
"""
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return None
|
||||
|
||||
try:
|
||||
dt = datetime.strptime(value, "%d %b %Y")
|
||||
return dt.strftime("%m/%d/%Y")
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def convert(in_path: Path, out_path: Path):
|
||||
with open(in_path, newline="", encoding="latin-1") as f_in, \
|
||||
open(out_path, "w", newline="", encoding="utf-8") as f_out:
|
||||
|
||||
reader = csv.reader(f_in)
|
||||
|
||||
# Skip metadata lines until we hit the header
|
||||
for row in reader:
|
||||
if row and row[0] == "Date":
|
||||
header = row
|
||||
break
|
||||
else:
|
||||
raise RuntimeError("Could not find Nationwide CSV header row")
|
||||
|
||||
dict_reader = csv.DictReader(f_in, fieldnames=header)
|
||||
|
||||
fieldnames = ["Date", "Payee", "Description", "Amount"]
|
||||
writer = csv.DictWriter(f_out, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
|
||||
for row in dict_reader:
|
||||
raw_date = row.get("Date") or ""
|
||||
date = parse_date(raw_date)
|
||||
if not date:
|
||||
continue
|
||||
|
||||
desc = (row.get("Description") or "").strip()
|
||||
payee = desc
|
||||
|
||||
paid_out = clean_money(row.get("Paid out", ""))
|
||||
paid_in = clean_money(row.get("Paid in", ""))
|
||||
|
||||
if paid_out is not None:
|
||||
amount = -paid_out
|
||||
elif paid_in is not None:
|
||||
amount = paid_in
|
||||
else:
|
||||
continue
|
||||
|
||||
writer.writerow({
|
||||
"Date": date,
|
||||
"Payee": payee,
|
||||
"Description": desc,
|
||||
"Amount": f"{amount:.2f}",
|
||||
})
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python nationwide-cleaner.py input.csv [output.csv]")
|
||||
sys.exit(1)
|
||||
|
||||
in_path = Path(sys.argv[1])
|
||||
out_path = Path(sys.argv[2]) if len(sys.argv) >= 3 else in_path.with_name(
|
||||
in_path.stem + "_actual.csv"
|
||||
)
|
||||
|
||||
convert(in_path, out_path)
|
||||
print(f"Written cleaned file to {out_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user