From ab2f4256aa0be08c159e16d1ce397d9eb3e4a95a Mon Sep 17 00:00:00 2001 From: vlado Date: Sun, 30 Nov 2025 19:37:24 +0100 Subject: [PATCH] reporter --- .idea/FIO.iml | 2 +- .idea/misc.xml | 1 + 2025-11-30 final reporter/.env | 5 + .../21ReadJSONmultipleaccounts.py | 353 ++++++++++++++++++ 2025-11-30 final reporter/30Report.py | 320 ++++++++++++++++ 2025-11-30 final reporter/40 test.py | 5 + 2025-11-30 final reporter/Functions.py | 108 ++++++ 2025-11-30 final reporter/RunFIOreport.py | 92 +++++ .../Whatsapptestsendin.py | 28 ++ 2025-11-30 final reporter/accounts.json | 53 +++ 2025-11-30 final reporter/logs/FIOreport.log | 164 ++++++++ 21 ReadJSONmultipleaccounts.py | 132 +++++-- 30 Report.py | 313 ++++++++++++++++ 31 Python.py | 7 + test_text.xlsx | Bin 0 -> 4837 bytes 15 files changed, 1554 insertions(+), 29 deletions(-) create mode 100644 2025-11-30 final reporter/.env create mode 100644 2025-11-30 final reporter/21ReadJSONmultipleaccounts.py create mode 100644 2025-11-30 final reporter/30Report.py create mode 100644 2025-11-30 final reporter/40 test.py create mode 100644 2025-11-30 final reporter/Functions.py create mode 100644 2025-11-30 final reporter/RunFIOreport.py create mode 100644 2025-11-30 final reporter/Whatsapptestsendin.py create mode 100644 2025-11-30 final reporter/accounts.json create mode 100644 2025-11-30 final reporter/logs/FIOreport.log create mode 100644 30 Report.py create mode 100644 31 Python.py create mode 100644 test_text.xlsx diff --git a/.idea/FIO.iml b/.idea/FIO.iml index 0a66021..e13954f 100644 --- a/.idea/FIO.iml +++ b/.idea/FIO.iml @@ -4,7 +4,7 @@ - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index a7ed4f6..7857915 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,4 +3,5 @@ + \ No newline at end of file diff --git a/2025-11-30 final reporter/.env b/2025-11-30 final reporter/.env new file mode 100644 index 0000000..a75e4c8 --- /dev/null +++ b/2025-11-30 final reporter/.env @@ -0,0 +1,5 @@ +WHATSAPP_TOKEN=EAAhOTShYLw4BQEh6HTO8fHmLHtbEXhZBEB03wgEXx1lVrcJkNHQlqPXZAlysqXIqse15bfL5V0kjNTnJ91kcK0DGkgpNtlzLaHbSgOdXaYUu9DOmSZACGAtqAj8nkSJz0ZA32Qz2BYJggyTxfkjwlT7rzMtGtbA9HwOA9AjEKG6JiiozBJmZCZA0nGBSx9JlHZCVQZDZD +PHONE_NUMBER_ID=420775735276 +WHATSAPP_PHONE_NUMBER_ID=930187756843231 +WHATSAPP_RECIPIENT_NUMBER=420775735276 +WHATSAPP_TEST_NUMBER=15551451876 diff --git a/2025-11-30 final reporter/21ReadJSONmultipleaccounts.py b/2025-11-30 final reporter/21ReadJSONmultipleaccounts.py new file mode 100644 index 0000000..40cd86f --- /dev/null +++ b/2025-11-30 final reporter/21ReadJSONmultipleaccounts.py @@ -0,0 +1,353 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +import io + +# Force UTF-8 output for Scheduled Tasks +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') +sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') + + +""" +FIO MULTI–ACCOUNT IMPORTER — FULLY COMMENTED VERSION +==================================================== + +This script downloads transactions for **multiple Fio bank accounts** +(using their API tokens) and imports them into a MySQL database +(`fio.transactions` table). + +It also saves the raw JSON responses into a folder structure +for backup / debugging / later use. + +Main features: + • Reads all accounts from accounts.json + • Downloads last N days (default 90) + • Saves JSON files to disk + • Extracts all transactions with safe parsing + • Inserts into MySQL with ON DUPLICATE KEY UPDATE + • Efficient batch insertion (executemany) +""" + +import os +import json +import time +from datetime import date, timedelta +from pathlib import Path + +import requests # used to call Fio REST API +import pymysql # MySQL driver + + +# ========================================= +# CONFIGURATION +# ========================================= + +# JSON file containing multiple account configs: +# [ +# { "name": "CZK rodina", "account_number": "2100046291", "token": "xxx" }, +# ... +# ] +ACCOUNTS_FILE = r"c:\users\vlado\PycharmProjects\FIO\accounts.json" + +# Directory where raw JSON files from Fio API will be stored. +JSON_BASE_DIR = r"z:\Dropbox\!!!Days\Downloads Z230\Fio" + +# MySQL connection parameters +DB = { + "host": "192.168.1.76", + "port": 3307, + "user": "root", + "password": "Vlado9674+", + "database": "fio", + "charset": "utf8mb4", +} + +# How many transactions insert per batch (performance tuning) +BATCH_SIZE = 500 + +# How many days back we load from Fio (default = last 90 days) +DAYS_BACK = 90 + + +# ========================================= +# HELPERS +# ========================================= + +def load_accounts(path: str): + """ + Reads accounts.json and does simple validation to ensure + each entry contains: name, account_number, token. + """ + with open(path, "r", encoding="utf-8") as f: + accounts = json.load(f) + + for acc in accounts: + for key in ("name", "account_number", "token"): + if key not in acc: + raise ValueError(f"Missing '{key}' in account config: {acc}") + + return accounts + + +def fio_url_for_period(token: str, d_from: date, d_to: date) -> str: + """ + Constructs the exact URL for Fio REST API "periods" endpoint. + Example: + https://fioapi.fio.cz/v1/rest/periods//2025-01-01/2025-01-31/transactions.json + """ + from_str = d_from.strftime("%Y-%m-%d") + to_str = d_to.strftime("%Y-%m-%d") + return f"https://fioapi.fio.cz/v1/rest/periods/{token}/{from_str}/{to_str}/transactions.json" + + +def fetch_fio_json(token: str, d_from: date, d_to: date): + """ + Calls Fio API and fetches JSON. + Handles HTTP errors and JSON decoding errors. + """ + url = fio_url_for_period(token, d_from, d_to) + resp = requests.get(url, timeout=30) + + if resp.status_code != 200: + print(f" ❌ HTTP {resp.status_code} from Fio: {url}") + return None + + try: + return resp.json() + except json.JSONDecodeError: + print(" ❌ Cannot decode JSON from Fio response") + return None + + +def safe_col(t: dict, n: int): + """ + SAFE ACCESSOR for Fio transaction column numbers. + + Fio JSON schema example: + "column5": { "name": "VS", "value": "123456" } + + But the structure is NOT guaranteed to exist. + So this function prevents KeyError or NoneType errors. + + Returns: + t["columnN"]["value"] or None + """ + key = f"column{n}" + val = t.get(key) + if not val: + return None + return val.get("value") + + +def clean_date(dt_str: str): + """ + Fio returns dates like: "2025-02-14+0100" + We strip timezone → "2025-02-14" + """ + if not dt_str: + return None + return dt_str[:10] + + +def ensure_dir(path: Path): + """Creates directory if it doesn’t exist.""" + path.mkdir(parents=True, exist_ok=True) + + +def save_json_for_account(base_dir: str, account_cfg: dict, data: dict, d_from: date, d_to: date): + """ + Saves raw JSON to: + //YYYY-MM-DD_to_YYYY-MM-DD.json + + Useful for debugging, backups, or re-imports. + """ + acc_num_raw = account_cfg["account_number"] + acc_folder_name = acc_num_raw.replace("/", "_") # sanitize dir name for filesystem + + out_dir = Path(base_dir) / acc_folder_name + ensure_dir(out_dir) + + filename = f"{d_from.strftime('%Y-%m-%d')}_to_{d_to.strftime('%Y-%m-%d')}.json" + out_path = out_dir / filename + + with open(out_path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + return out_path + + +# ========================================= +# MAIN IMPORT LOGIC +# ========================================= + +def main(): + start_all = time.time() + + # Calculate time range (last N days) + today = date.today() + d_from = today - timedelta(days=DAYS_BACK) + d_to = today + + print("=== Fio multi-account import ===") + print(f"Období: {d_from} až {d_to}") + print("Načítám účty z JSON konfigurace...") + + # Load all accounts from accounts.json + accounts = load_accounts(ACCOUNTS_FILE) + print(f" Účtů v konfiguraci: {len(accounts)}\n") + + # Connect to database + conn = pymysql.connect(**DB) + cur = conn.cursor() + + # SQL INSERT with ON DUPLICATE KEY UPDATE + # This means: if transaction already exists (same unique key), update it. + sql = """ + INSERT INTO transactions + ( + datum, objem, mena, cislo_uctu, protiucet, kod_banky, + ks, vs, ss, zprava_pro_prijemce, poznamka, + id_operace, id_pokynu, ks_1, nazev_banky, nazev_protiuctu, + ss_1, typ, upresneni_objem, upresneni_mena, vs_1, zadal + ) + VALUES + ( + %(datum)s, %(objem)s, %(mena)s, %(cislo_uctu)s, %(protiucet)s, %(kod_banky)s, + %(ks)s, %(vs)s, %(ss)s, %(zprava)s, %(poznamka)s, + %(id_operace)s, %(id_pokynu)s, %(ks1)s, %(nazev_banky)s, %(nazev_protiuctu)s, + %(ss1)s, %(typ)s, %(upr_objem)s, %(upr_mena)s, %(vs1)s, %(zadal)s + ) + ON DUPLICATE KEY UPDATE + datum = VALUES(datum), + objem = VALUES(objem), + mena = VALUES(mena), + protiucet = VALUES(protiucet), + kod_banky = VALUES(kod_banky), + ks = VALUES(ks), + vs = VALUES(vs), + ss = VALUES(ss), + zprava_pro_prijemce = VALUES(zprava_pro_prijemce), + poznamka = VALUES(poznamka), + ks_1 = VALUES(ks_1), + nazev_banky = VALUES(nazev_banky), + nazev_protiuctu = VALUES(nazev_protiuctu), + ss_1 = VALUES(ss_1), + typ = VALUES(typ), + upresneni_objem = VALUES(upresneni_objem), + upresneni_mena = VALUES(upresneni_mena), + vs_1 = VALUES(vs_1), + zadal = VALUES(zadal) + """ + + total_inserted = 0 + + # ====================================================== + # PROCESS EACH ACCOUNT IN accounts.json + # ====================================================== + for acc in accounts: + name = acc["name"] + cfg_acc_num = acc["account_number"] + token = acc["token"] + + print(f"--- Účet: {name} ({cfg_acc_num}) ---") + t0 = time.time() + + # --- 1) Download JSON from Fio API + data = fetch_fio_json(token, d_from, d_to) + if data is None: + print(" Přeskakuji, žádná data / chyba API.\n") + continue + + # --- 2) Save raw JSON file to disk + json_path = save_json_for_account(JSON_BASE_DIR, acc, data, d_from, d_to) + print(f" JSON uložen do: {json_path}") + + # --- 3) Extract transactions from JSON tree + tlist = data["accountStatement"]["transactionList"].get("transaction", []) + + # FIO can return single transaction as an object (not list) + if isinstance(tlist, dict): + tlist = [tlist] + + print(f" Počet transakcí v období: {len(tlist)}") + + if not tlist: + print(" Žádné transakce, jdu dál.\n") + continue + + # FIO returns account ID under accountStatement.info.accountId + fio_acc_id = data["accountStatement"]["info"]["accountId"] + + # Warn if account ID in JSON doesn't match config (informational only) + if cfg_acc_num and cfg_acc_num.split("/")[0] not in fio_acc_id: + print(f" ⚠ Upozornění: accountId z Fio ({fio_acc_id}) " + f"se neshoduje s account_number v konfiguraci ({cfg_acc_num})") + + # --- 4) Build list of MySQL rows + rows = [] + for t in tlist: + row = { + "datum": clean_date(safe_col(t, 0)), + "objem": safe_col(t, 1), + "mena": safe_col(t, 14), + + "cislo_uctu": fio_acc_id, + "protiucet": safe_col(t, 2), + "kod_banky": safe_col(t, 3), + + "ks": safe_col(t, 4), + "vs": safe_col(t, 5), + "ss": safe_col(t, 6), + + "zprava": safe_col(t, 16), + "poznamka": safe_col(t, 25), + + "id_operace": safe_col(t, 22), + "id_pokynu": safe_col(t, 24), + + "ks1": safe_col(t, 18), + "nazev_banky": safe_col(t, 15), + "nazev_protiuctu": safe_col(t, 10), + + "ss1": safe_col(t, 19), + "typ": safe_col(t, 8), + + "upr_objem": safe_col(t, 20), + "upr_mena": safe_col(t, 21), + "vs1": safe_col(t, 17), + + "zadal": safe_col(t, 12), + } + rows.append(row) + + # --- 5) INSERT rows into MySQL in batches + inserted = 0 + + for i in range(0, len(rows), BATCH_SIZE): + chunk = rows[i : i + BATCH_SIZE] + cur.executemany(sql, chunk) # fast multi-row insert/update + conn.commit() + inserted += len(chunk) + + elapsed = time.time() - t0 + total_inserted += inserted + + print(f" ✓ Zapsáno (insert/update): {inserted} řádků do DB za {elapsed:.2f} s\n") + + # Close DB + cur.close() + conn.close() + + total_elapsed = time.time() - start_all + + print(f"=== Hotovo. Celkem zapsáno {total_inserted} transakcí. " + f"Celkový čas: {total_elapsed:.2f} s ===") + + +# ====================================================== +# ENTRY POINT +# ====================================================== + +if __name__ == "__main__": + main() diff --git a/2025-11-30 final reporter/30Report.py b/2025-11-30 final reporter/30Report.py new file mode 100644 index 0000000..f817281 --- /dev/null +++ b/2025-11-30 final reporter/30Report.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +import io + +# Force UTF-8 output for Scheduled Tasks +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') +sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') + +""" +FIO EXPORT SCRIPT — FULLY COMMENTED VERSION +------------------------------------------- + +This script connects to your MySQL "fio" database, +reads all transactions, and exports them into a highly formatted +Excel workbook. + +Excel file includes: + + • First sheet: "ALL" → contains ALL transactions + • Additional sheets: one for each account from accounts.json + • First 5 CZK sheets appear first in custom order + • All formatting exactly preserved (colors, borders, widths, formulas) + +Everything is generated automatically. +""" + +import mysql.connector +from mysql.connector import Error +from openpyxl import Workbook +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side +from datetime import datetime +import os +import glob +import json + +# ====================================================== +# CONFIGURATION +# ====================================================== + +# MySQL server parameters +DB_HOST = "192.168.1.76" +DB_PORT = 3307 +DB_USER = "root" +DB_PASS = "Vlado9674+" +DB_NAME = "fio" + +# Where to save Excel files +OUTPUT_DIR = r"Z:\Dropbox\!!!Days\Downloads Z230" + +# JSON file with list of accounts (name + account_number) +ACCOUNTS_JSON = r"C:\Users\vlado\PycharmProjects\FIO\accounts.json" + +# Columns that MUST be written as TEXT in Excel using ="value" +# to avoid Excel stripping zeros or changing formatting +TEXT_COLUMNS = ["cislo_uctu", "protiucet", "kod_banky", "ks", "vs", "ss"] + + +# ====================================================== +# REMOVE OLD EXPORT FILES +# ====================================================== + +def cleanup_old_exports(): + """ + Deletes older versions of exported XLSX files that match + specific filename patterns. This keeps your folder clean, + ensuring you only have the most recent export. + """ + patterns = [ + os.path.join(OUTPUT_DIR, "*FIO*transaction*.xlsx"), + os.path.join(OUTPUT_DIR, "*FIO*transactions*.xlsx"), + os.path.join(OUTPUT_DIR, "*FIO_transactions*.xlsx"), + ] + + # Check each pattern + for pattern in patterns: + for file in glob.glob(pattern): + try: + os.remove(file) + print(f"🗑 Deleted old export: {file}") + except: + # If file cannot be deleted (locked or permission denied), + # simply skip it. + pass + + +# ====================================================== +# CORE EXCEL FORMATTING FUNCTION +# ====================================================== + +def format_sheet(ws, rows, headers): + """ + Applies ALL formatting rules to a worksheet: + - Writes headers + - Writes all rows + - Converts selected columns to Excel text formulas + - Colors rows based on "objem" (red=negative, green=positive) + - Sets fixed column widths + - Adds borders to every cell + - Center-aligns first 10 columns + - Freezes header row and enables filtering + """ + + # ------------------------------- + # 1) Format HEADER row + # ------------------------------- + for col_idx in range(1, len(headers) + 1): + cell = ws.cell(row=1, column=col_idx) + cell.font = Font(bold=True) # bold text + cell.fill = PatternFill(start_color="FFFF00", fill_type="solid") # yellow background + + # ------------------------------- + # 2) Write DATA rows + # ------------------------------- + for row in rows: + excel_row = [] + for h in headers: + val = row[h] + + # For text-sensitive columns, write ="value" + # This prevents Excel from stripping zeros or treating them as numbers. + if h in TEXT_COLUMNS and val is not None: + excel_row.append(f'="{val}"') + else: + excel_row.append(val) + + ws.append(excel_row) + + # ------------------------------- + # 3) Background coloring by "objem" + # ------------------------------- + # Light red (ARGB) = negative + fill_red = PatternFill(start_color="FFFFDDDD", end_color="FFFFDDDD", fill_type="solid") + # Light green (ARGB) = positive or zero + fill_green = PatternFill(start_color="FFEEFFEE", end_color="FFEEFFEE", fill_type="solid") + + # Find column index where "objem" is located + objem_col_index = headers.index("objem") + 1 + + # Apply row coloring + for row_idx in range(2, len(rows) + 2): # Start at row 2 (row 1 = header) + cell_objem = ws.cell(row=row_idx, column=objem_col_index) + + # Convert objem to float + try: + value = float(cell_objem.value) + except: + value = 0 + + # Choose correct color + fill = fill_red if value < 0 else fill_green + + # Apply fill to entire row + for col_idx in range(1, len(headers) + 1): + ws.cell(row=row_idx, column=col_idx).fill = fill + + # ------------------------------- + # 4) Fixed column widths + # ------------------------------- + fixed_widths = [ + 6, 11, 11, 5, 14, 14, 8, 6, 13, 13, + 50, 53, 12, 12, 5, 49, 29, 5, 29, 16, + 15, 12, 49, 20 + ] + + # Apply width using A, B, C... column names + for i, width in enumerate(fixed_widths, start=1): + col_letter = chr(64 + i) # convert 1 → 'A', 2 → 'B', ... + ws.column_dimensions[col_letter].width = width + + # ------------------------------- + # 5) Add borders + alignment + # ------------------------------- + thin = Side(border_style="thin", color="000000") + border = Border(left=thin, right=thin, top=thin, bottom=thin) + align_center = Alignment(horizontal="center") + + total_rows = len(rows) + 1 + total_cols = len(headers) + + for row_idx in range(1, total_rows + 1): + for col_idx in range(1, total_cols + 1): + cell = ws.cell(row=row_idx, column=col_idx) + cell.border = border # add border + + # Center-align ONLY first 10 columns + if col_idx <= 10: + cell.alignment = align_center + + # Freeze header row so it stays visible while scrolling + ws.freeze_panes = "A2" + + # Enable auto filter on top row + ws.auto_filter.ref = ws.dimensions + + +# ====================================================== +# MAIN EXPORT PROCESS +# ====================================================== + +def export_fio(): + print("Connecting to MySQL...") + + # Connect to MySQL database + try: + conn = mysql.connector.connect( + host=DB_HOST, + port=DB_PORT, + user=DB_USER, + password=DB_PASS, + database=DB_NAME + ) + except Error as e: + print("❌ Failed to connect:", e) + return + + cur = conn.cursor(dictionary=True) + + # ------------------------------- + # Load accounts.json + # ------------------------------- + with open(ACCOUNTS_JSON, "r", encoding="utf-8") as f: + accounts = json.load(f) + + # ------------------------------- + # Define priority first sheets + # ------------------------------- + preferred_order = [ + "CZK rodina", + "CZK ordinace", + "CZK na jídlo", + "CZK TrialHelp", + "CZK maminka svojě věci" + ] + + accounts_sorted = [] + + # Step 1: add priority accounts first + for pref in preferred_order: + for acc in accounts: + if acc["name"] == pref: + accounts_sorted.append(acc) + + # Step 2: add remaining accounts afterward + for acc in accounts: + if acc not in accounts_sorted: + accounts_sorted.append(acc) + + # ------------------------------- + # Create a new Excel workbook + # ------------------------------- + wb = Workbook() + wb.remove(wb.active) # remove default empty sheet + + # ------------------------------- + # FIRST SHEET: ALL TRANSACTIONS + # ------------------------------- + cur.execute("SELECT * FROM transactions ORDER BY datum DESC") + all_rows = cur.fetchall() + + if all_rows: + headers = list(all_rows[0].keys()) + ws_all = wb.create_sheet(title="ALL") + ws_all.append(headers) + format_sheet(ws_all, all_rows, headers) + + # ------------------------------- + # INDIVIDUAL SHEETS PER ACCOUNT + # ------------------------------- + for acc in accounts_sorted: + acc_num = acc["account_number"] + sheet_name = acc["name"][:31] # Excel sheet name limit + + print(f"➡ Creating sheet: {sheet_name}") + + query = f""" + SELECT * + FROM transactions + WHERE cislo_uctu = '{acc_num}' + ORDER BY datum DESC + """ + + cur.execute(query) + rows = cur.fetchall() + + if not rows: + print(f"⚠ No data for {sheet_name}") + continue + + headers = list(rows[0].keys()) + ws = wb.create_sheet(title=sheet_name) + ws.append(headers) + + format_sheet(ws, rows, headers) + + conn.close() + + # ------------------------------- + # Save Excel file + # ------------------------------- + + cleanup_old_exports() + + # File name includes timestamp + timestamp = datetime.now().strftime("%Y-%m-%d %H-%M-%S") + filename = f"{timestamp} FIO transactions.xlsx" + output_file = os.path.join(OUTPUT_DIR, filename) + + wb.save(output_file) + + print(f"✅ Export complete:\n{output_file}") + + +# ====================================================== +# MAIN ENTRY POINT +# ====================================================== + +if __name__ == "__main__": + export_fio() diff --git a/2025-11-30 final reporter/40 test.py b/2025-11-30 final reporter/40 test.py new file mode 100644 index 0000000..88f6091 --- /dev/null +++ b/2025-11-30 final reporter/40 test.py @@ -0,0 +1,5 @@ +from Functions import SendWhatsAppMessage, get_dropbox_root + +SendWhatsAppMessage("Ahoj Vlado, úloha dokončena!") + +print(get_dropbox_root()) \ No newline at end of file diff --git a/2025-11-30 final reporter/Functions.py b/2025-11-30 final reporter/Functions.py new file mode 100644 index 0000000..312161f --- /dev/null +++ b/2025-11-30 final reporter/Functions.py @@ -0,0 +1,108 @@ +# Function.py +import os +import time +import requests +from dotenv import load_dotenv + +# Load .env variables once +load_dotenv() + +WHATSAPP_TOKEN = os.getenv("WHATSAPP_TOKEN") +WHATSAPP_PHONE_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID") +WHATSAPP_RECIPIENT = os.getenv("WHATSAPP_RECIPIENT_NUMBER") + +WAPI_URL = f"https://graph.facebook.com/v21.0/{WHATSAPP_PHONE_ID}/messages" + + +def SendWhatsAppMessage(message: str, retries: int = 3, delay: int = 2) -> bool: + """ + Sends a WhatsApp message using the WhatsApp Cloud API test number. + Automatically retries on failure. + + :param message: Text to send. + :param retries: Number of retry attempts. + :param delay: Delay between retries (seconds). + :return: True if message sent successfully, False otherwise. + """ + + # --- safety check: missing config --- + if not WHATSAPP_TOKEN or not WHATSAPP_PHONE_ID or not WHATSAPP_RECIPIENT: + print("❌ WhatsApp API configuration missing in .env") + return False + + headers = { + "Authorization": f"Bearer {WHATSAPP_TOKEN}", + "Content-Type": "application/json" + } + + payload = { + "messaging_product": "whatsapp", + "to": WHATSAPP_RECIPIENT, + "type": "text", + "text": {"body": message} + } + + # --- retry loop --- + for attempt in range(1, retries + 1): + try: + response = requests.post(WAPI_URL, headers=headers, json=payload, timeout=15) + status = response.status_code + + if status == 200: + print(f"📨 WhatsApp message sent successfully (attempt {attempt})") + return True + + else: + print(f"⚠️ WhatsApp API error (attempt {attempt}): {status} {response.text}") + + except requests.RequestException as e: + print(f"⚠️ Network error (attempt {attempt}): {e}") + + time.sleep(delay) + + print("❌ Failed to send WhatsApp message after retries.") + return False + +# ----------------------------------------------------- +# Find Dropbox root by reading official info.json +# ----------------------------------------------------- + +def get_dropbox_root() -> str | None: + # """ + # Returns the absolute Dropbox folder path by reading: + # C:\Users\\AppData\Local\Dropbox\info.json + # This is 100% reliable even if Dropbox changes drive letter. + # """ + + import os + import json + + localapp = os.environ.get("LOCALAPPDATA") + if not localapp: + print("⚠️ LOCALAPPDATA not found.") + return None + + info_path = os.path.join(localapp, "Dropbox", "info.json") + + if not os.path.exists(info_path): + print(f"⚠️ Dropbox info.json not found at: {info_path}") + return None + + try: + with open(info_path, "r", encoding="utf-8") as f: + data = json.load(f) + + # Most users: `personal` + if "personal" in data and "path" in data["personal"]: + return data["personal"]["path"] + + # Business Dropbox if used + if "business" in data and "path" in data["business"]: + return data["business"]["path"] + + print("⚠️ Dropbox info.json missing 'path' in personal/business") + return None + + except Exception as e: + print(f"⚠️ Error reading Dropbox info.json: {e}") + return None diff --git a/2025-11-30 final reporter/RunFIOreport.py b/2025-11-30 final reporter/RunFIOreport.py new file mode 100644 index 0000000..efae1da --- /dev/null +++ b/2025-11-30 final reporter/RunFIOreport.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys +import subprocess +import time +from datetime import datetime + +import sys +import io + +# Force UTF-8 output for Scheduled Tasks +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') +sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') + + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + +SCRIPT1 = os.path.join(BASE_DIR, "21ReadJSONmultipleaccounts.py") +SCRIPT2 = os.path.join(BASE_DIR, "30Report.py") +LOG_DIR = os.path.join(BASE_DIR, "logs") +LOG_FILE = os.path.join(LOG_DIR, "FIOreport.log") + +os.makedirs(LOG_DIR, exist_ok=True) + +# Optional WhatsApp notify +try: + from Functions import SendWhatsAppMessage + WHATSAPP_AVAILABLE = True +except Exception: + WHATSAPP_AVAILABLE = False + + +def write_log(text): + with open(LOG_FILE, "a", encoding="utf-8") as f: + f.write(text + "\n") + print(text) + + +def run_script(path): + write_log(f"\n[{datetime.now()}] ➡ Running: {os.path.basename(path)}") + + if not os.path.isfile(path): + write_log(f"❌ Script not found: {path}") + return False + + process = subprocess.Popen( + [sys.executable, path], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + encoding="utf-8", + errors="replace" + ) + + # Log STDOUT live + for line in process.stdout: + write_log(line.rstrip()) + + # Log STDERR live + for line in process.stderr: + write_log("⚠️ " + line.rstrip()) + + process.wait() + return process.returncode == 0 + + +# ---------------------------------------------------------- +# MAIN +# ---------------------------------------------------------- +if __name__ == "__main__": + write_log("\n====================== NEW RUN ======================") + + ok1 = run_script(SCRIPT1) + ok2 = False + + if ok1: + write_log("✔ Stage 1 OK") + time.sleep(1) + ok2 = run_script(SCRIPT2) + + if ok1 and ok2: + write_log("✔ All stages completed successfully") + if WHATSAPP_AVAILABLE: + SendWhatsAppMessage("✔ FIO import + report hotový.") + else: + write_log("❌ SOME PART FAILED — check above for errors") + if WHATSAPP_AVAILABLE: + SendWhatsAppMessage("❌ FIO proces selhal. Zkontroluj log.") + + write_log("======================== END ========================\n") diff --git a/2025-11-30 final reporter/Whatsapptestsendin.py b/2025-11-30 final reporter/Whatsapptestsendin.py new file mode 100644 index 0000000..9f9a658 --- /dev/null +++ b/2025-11-30 final reporter/Whatsapptestsendin.py @@ -0,0 +1,28 @@ +import os +import requests +from dotenv import load_dotenv + +load_dotenv() + +TOKEN = os.getenv("WHATSAPP_TOKEN") +PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID") +RECIPIENT = os.getenv("WHATSAPP_RECIPIENT_NUMBER") + +def send_whatsapp_message(text): + url = f"https://graph.facebook.com/v22.0/{PHONE_NUMBER_ID}/messages" + headers = { + "Authorization": f"Bearer {TOKEN}", + "Content-Type": "application/json" + } + data = { + "messaging_product": "whatsapp", + "to": RECIPIENT, + "type": "text", + "text": {"body": text} + } + + r = requests.post(url, headers=headers, json=data) + print("Status:", r.status_code) + print("Response:", r.text) + +send_whatsapp_message("Ahoj Vlado! Test zpráva přes WhatsApp API 🔔") diff --git a/2025-11-30 final reporter/accounts.json b/2025-11-30 final reporter/accounts.json new file mode 100644 index 0000000..101357a --- /dev/null +++ b/2025-11-30 final reporter/accounts.json @@ -0,0 +1,53 @@ +[ + { + "name": "EUR tatínek 1", + "account_number": "2100074583", + "token": "GuV2Boaulx56ZiQUqUArgg6P9qdfEVKOoH6wF3PfAZ0fPS01r2WbiNiCsCcIBZ0U" + }, + { + "name": "CZK rodina", + "account_number": "2100046291", + "token": "v0GJaAVeefzV1lnx1jPCf2nFF7SuOPzzrL5tobPNsC7oCChXG4hahDYVb8Rdcex0" + }, + { + "name": "EUR TrialHelp", + "account_number": "2200787265", + "token": "9yG5g6lHWGS6YU2R2petm5DRYTb9orhJ8VPJ0p7RtTjlIo2vB83ynBlPCMGRIwzy" + }, + { + "name": "CZK tatínek", + "account_number": "2400046293", + "token": "j2qmpvWe4RfKtBTBlhwC1VFED7HJlVAe23iPBH1TWis9htEyYe8fRejcMeSxOLqC" + }, + { + "name": "CHF tatínek", + "account_number": "2402161017", + "token": "aNfK9iu6qIPlugGCR6gvSJ7NXtTkDfVVj8fBz4X1pORuGKf6VXjWin4wrr9WRjSd" + }, + { + "name": "EUR tatínek 2", + "account_number": "2500074582", + "token": "aLsl9ETRUU1IgoYeinAzYWyruIoJvs6UvJKTGRlJcm7HaEc5ojsFdxJizyT9lREO" + }, + { + "name": "CZK TrialHelp", + "account_number": "2900046548", + "token": "pKZVHbFDVsbTa8ryEaVc6A2nyrlb4TbT1tCiimieesHvhKFoJmYBRVjCpnvjiUUK" + }, + { + "name": "CZK maminka svojě věci", + "account_number": "2003310572", + "token": "TkrRvnMK77OSSYdVulNvZcT6ltWcmjqkp3RN5WYwnBpNTuaKCWO1zHKOlDGAiNyv" + }, + { + "name": "CZK na jídlo", + "account_number": "2403310563", + "token": "axRvFxu4VCzsDp5QZXN8LQ0fQUqzV2FEBZrM595x3Rtp10zowRBcGOFs9uNNPb7Q" + }, + { + "name": "CZK ordinace", + "account_number": "2800046620", + "token": "Xzdr3eK7se7ZgeE3JujgeidGb0WrB7mGQ6HSOiBJzWi0kPURYKRpkRKB3ZOpt3rq" + } +] + diff --git a/2025-11-30 final reporter/logs/FIOreport.log b/2025-11-30 final reporter/logs/FIOreport.log new file mode 100644 index 0000000..3b6fbe0 --- /dev/null +++ b/2025-11-30 final reporter/logs/FIOreport.log @@ -0,0 +1,164 @@ + +====================== NEW RUN ====================== + +[2025-11-30 19:25:09.332782] ➡ Running: 21ReadJSONmultipleaccounts.py +=== Fio multi-account import === +Období: 2025-09-01 až 2025-11-30 +Načítám účty z JSON konfigurace... + Účtů v konfiguraci: 10 + +--- Účet: EUR tatínek 1 (2100074583) --- + JSON uložen do: z:\Dropbox\!!!Days\Downloads Z230\Fio\2100074583\2025-09-01_to_2025-11-30.json + Počet transakcí v období: 2 + ✓ Zapsáno (insert/update): 2 řádků do DB za 0.27 s + +--- Účet: CZK rodina (2100046291) --- + JSON uložen do: z:\Dropbox\!!!Days\Downloads Z230\Fio\2100046291\2025-09-01_to_2025-11-30.json + Počet transakcí v období: 307 + ✓ Zapsáno (insert/update): 307 řádků do DB za 0.37 s + +--- Účet: EUR TrialHelp (2200787265) --- + JSON uložen do: z:\Dropbox\!!!Days\Downloads Z230\Fio\2200787265\2025-09-01_to_2025-11-30.json + Počet transakcí v období: 2 + ✓ Zapsáno (insert/update): 2 řádků do DB za 0.22 s + +--- Účet: CZK tatínek (2400046293) --- + JSON uložen do: z:\Dropbox\!!!Days\Downloads Z230\Fio\2400046293\2025-09-01_to_2025-11-30.json + Počet transakcí v období: 0 + Žádné transakce, jdu dál. + +--- Účet: CHF tatínek (2402161017) --- + JSON uložen do: z:\Dropbox\!!!Days\Downloads Z230\Fio\2402161017\2025-09-01_to_2025-11-30.json + Počet transakcí v období: 0 + Žádné transakce, jdu dál. + +--- Účet: EUR tatínek 2 (2500074582) --- + JSON uložen do: z:\Dropbox\!!!Days\Downloads Z230\Fio\2500074582\2025-09-01_to_2025-11-30.json + Počet transakcí v období: 0 + Žádné transakce, jdu dál. + +--- Účet: CZK TrialHelp (2900046548) --- + JSON uložen do: z:\Dropbox\!!!Days\Downloads Z230\Fio\2900046548\2025-09-01_to_2025-11-30.json + Počet transakcí v období: 6 + ✓ Zapsáno (insert/update): 6 řádků do DB za 0.23 s + +--- Účet: CZK maminka svojě věci (2003310572) --- + JSON uložen do: z:\Dropbox\!!!Days\Downloads Z230\Fio\2003310572\2025-09-01_to_2025-11-30.json + Počet transakcí v období: 75 + ✓ Zapsáno (insert/update): 75 řádků do DB za 0.27 s + +--- Účet: CZK na jídlo (2403310563) --- + JSON uložen do: z:\Dropbox\!!!Days\Downloads Z230\Fio\2403310563\2025-09-01_to_2025-11-30.json + Počet transakcí v období: 119 + ✓ Zapsáno (insert/update): 119 řádků do DB za 0.38 s + +--- Účet: CZK ordinace (2800046620) --- + JSON uložen do: z:\Dropbox\!!!Days\Downloads Z230\Fio\2800046620\2025-09-01_to_2025-11-30.json + Počet transakcí v období: 126 + ✓ Zapsáno (insert/update): 126 řádků do DB za 0.31 s + +=== Hotovo. Celkem zapsáno 637 transakcí. Celkový čas: 2.94 s === +⚠️ Could not find platform independent libraries +✔ Stage 1 OK + +[2025-11-30 19:25:13.462554] ➡ Running: 30Report.py +Connecting to MySQL... +➡ Creating sheet: CZK rodina +➡ Creating sheet: CZK ordinace +➡ Creating sheet: CZK na jídlo +➡ Creating sheet: CZK TrialHelp +➡ Creating sheet: CZK maminka svojě věci +➡ Creating sheet: EUR tatínek 1 +➡ Creating sheet: EUR TrialHelp +➡ Creating sheet: CZK tatínek +➡ Creating sheet: CHF tatínek +➡ Creating sheet: EUR tatínek 2 +🗑 Deleted old export: Z:\Dropbox\!!!Days\Downloads Z230\2025-11-30 19-22-48 FIO transactions.xlsx +✅ Export complete: +Z:\Dropbox\!!!Days\Downloads Z230\2025-11-30 19-25-54 FIO transactions.xlsx +⚠️ Could not find platform independent libraries +✔ All stages completed successfully +======================== END ======================== + + +====================== NEW RUN ====================== + +[2025-11-30 19:30:19.846254] ➡ Running: 21ReadJSONmultipleaccounts.py +=== Fio multi-account import === +Období: 2025-09-01 až 2025-11-30 +Načítám účty z JSON konfigurace... + Účtů v konfiguraci: 10 + +--- Účet: EUR tatínek 1 (2100074583) --- + JSON uložen do: z:\Dropbox\!!!Days\Downloads Z230\Fio\2100074583\2025-09-01_to_2025-11-30.json + Počet transakcí v období: 2 + ✓ Zapsáno (insert/update): 2 řádků do DB za 0.26 s + +--- Účet: CZK rodina (2100046291) --- + JSON uložen do: z:\Dropbox\!!!Days\Downloads Z230\Fio\2100046291\2025-09-01_to_2025-11-30.json + Počet transakcí v období: 307 + ✓ Zapsáno (insert/update): 307 řádků do DB za 0.37 s + +--- Účet: EUR TrialHelp (2200787265) --- + JSON uložen do: z:\Dropbox\!!!Days\Downloads Z230\Fio\2200787265\2025-09-01_to_2025-11-30.json + Počet transakcí v období: 2 + ✓ Zapsáno (insert/update): 2 řádků do DB za 0.23 s + +--- Účet: CZK tatínek (2400046293) --- + JSON uložen do: z:\Dropbox\!!!Days\Downloads Z230\Fio\2400046293\2025-09-01_to_2025-11-30.json + Počet transakcí v období: 0 + Žádné transakce, jdu dál. + +--- Účet: CHF tatínek (2402161017) --- + JSON uložen do: z:\Dropbox\!!!Days\Downloads Z230\Fio\2402161017\2025-09-01_to_2025-11-30.json + Počet transakcí v období: 0 + Žádné transakce, jdu dál. + +--- Účet: EUR tatínek 2 (2500074582) --- + JSON uložen do: z:\Dropbox\!!!Days\Downloads Z230\Fio\2500074582\2025-09-01_to_2025-11-30.json + Počet transakcí v období: 0 + Žádné transakce, jdu dál. + +--- Účet: CZK TrialHelp (2900046548) --- + JSON uložen do: z:\Dropbox\!!!Days\Downloads Z230\Fio\2900046548\2025-09-01_to_2025-11-30.json + Počet transakcí v období: 6 + ✓ Zapsáno (insert/update): 6 řádků do DB za 0.31 s + +--- Účet: CZK maminka svojě věci (2003310572) --- + JSON uložen do: z:\Dropbox\!!!Days\Downloads Z230\Fio\2003310572\2025-09-01_to_2025-11-30.json + Počet transakcí v období: 75 + ✓ Zapsáno (insert/update): 75 řádků do DB za 0.37 s + +--- Účet: CZK na jídlo (2403310563) --- + JSON uložen do: z:\Dropbox\!!!Days\Downloads Z230\Fio\2403310563\2025-09-01_to_2025-11-30.json + Počet transakcí v období: 119 + ✓ Zapsáno (insert/update): 119 řádků do DB za 0.30 s + +--- Účet: CZK ordinace (2800046620) --- + JSON uložen do: z:\Dropbox\!!!Days\Downloads Z230\Fio\2800046620\2025-09-01_to_2025-11-30.json + Počet transakcí v období: 126 + ✓ Zapsáno (insert/update): 126 řádků do DB za 0.30 s + +=== Hotovo. Celkem zapsáno 637 transakcí. Celkový čas: 2.81 s === +⚠️ Could not find platform independent libraries +✔ Stage 1 OK + +[2025-11-30 19:30:23.822641] ➡ Running: 30Report.py +Connecting to MySQL... +➡ Creating sheet: CZK rodina +➡ Creating sheet: CZK ordinace +➡ Creating sheet: CZK na jídlo +➡ Creating sheet: CZK TrialHelp +➡ Creating sheet: CZK maminka svojě věci +➡ Creating sheet: EUR tatínek 1 +➡ Creating sheet: EUR TrialHelp +➡ Creating sheet: CZK tatínek +➡ Creating sheet: CHF tatínek +➡ Creating sheet: EUR tatínek 2 +🗑 Deleted old export: Z:\Dropbox\!!!Days\Downloads Z230\2025-11-30 19-25-54 FIO transactions.xlsx +✅ Export complete: +Z:\Dropbox\!!!Days\Downloads Z230\2025-11-30 19-31-02 FIO transactions.xlsx +⚠️ Could not find platform independent libraries +✔ All stages completed successfully +======================== END ======================== + diff --git a/21 ReadJSONmultipleaccounts.py b/21 ReadJSONmultipleaccounts.py index 5da1332..efea445 100644 --- a/21 ReadJSONmultipleaccounts.py +++ b/21 ReadJSONmultipleaccounts.py @@ -1,42 +1,79 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +""" +FIO MULTI–ACCOUNT IMPORTER — FULLY COMMENTED VERSION +==================================================== + +This script downloads transactions for **multiple Fio bank accounts** +(using their API tokens) and imports them into a MySQL database +(`fio.transactions` table). + +It also saves the raw JSON responses into a folder structure +for backup / debugging / later use. + +Main features: + • Reads all accounts from accounts.json + • Downloads last N days (default 90) + • Saves JSON files to disk + • Extracts all transactions with safe parsing + • Inserts into MySQL with ON DUPLICATE KEY UPDATE + • Efficient batch insertion (executemany) +""" + import os import json import time from datetime import date, timedelta from pathlib import Path -import requests -import pymysql +import requests # used to call Fio REST API +import pymysql # MySQL driver + # ========================================= -# CONFIG +# CONFIGURATION # ========================================= -ACCOUNTS_FILE = r"u:\PycharmProjects\FIO\accounts.json" -JSON_BASE_DIR = r"u:\Dropbox\!!!Days\Downloads Z230\Fio" # kam se budou ukládat JSONy +# JSON file containing multiple account configs: +# [ +# { "name": "CZK rodina", "account_number": "2100046291", "token": "xxx" }, +# ... +# ] +ACCOUNTS_FILE = r"c:\users\vlado\PycharmProjects\FIO\accounts.json" + +# Directory where raw JSON files from Fio API will be stored. +JSON_BASE_DIR = r"z:\Dropbox\!!!Days\Downloads Z230\Fio" + +# MySQL connection parameters DB = { "host": "192.168.1.76", "port": 3307, "user": "root", - "password": "Vlado9674+", # uprav podle sebe / dej do .env + "password": "Vlado9674+", "database": "fio", "charset": "utf8mb4", } +# How many transactions insert per batch (performance tuning) BATCH_SIZE = 500 + +# How many days back we load from Fio (default = last 90 days) DAYS_BACK = 90 # ========================================= # HELPERS # ========================================= + def load_accounts(path: str): + """ + Reads accounts.json and does simple validation to ensure + each entry contains: name, account_number, token. + """ with open(path, "r", encoding="utf-8") as f: accounts = json.load(f) - # jednoduchá validace for acc in accounts: for key in ("name", "account_number", "token"): if key not in acc: @@ -46,17 +83,28 @@ def load_accounts(path: str): def fio_url_for_period(token: str, d_from: date, d_to: date) -> str: + """ + Constructs the exact URL for Fio REST API "periods" endpoint. + Example: + https://fioapi.fio.cz/v1/rest/periods//2025-01-01/2025-01-31/transactions.json + """ from_str = d_from.strftime("%Y-%m-%d") to_str = d_to.strftime("%Y-%m-%d") return f"https://fioapi.fio.cz/v1/rest/periods/{token}/{from_str}/{to_str}/transactions.json" def fetch_fio_json(token: str, d_from: date, d_to: date): + """ + Calls Fio API and fetches JSON. + Handles HTTP errors and JSON decoding errors. + """ url = fio_url_for_period(token, d_from, d_to) resp = requests.get(url, timeout=30) + if resp.status_code != 200: print(f" ❌ HTTP {resp.status_code} from Fio: {url}") return None + try: return resp.json() except json.JSONDecodeError: @@ -66,11 +114,16 @@ def fetch_fio_json(token: str, d_from: date, d_to: date): def safe_col(t: dict, n: int): """ - Safely read t['columnN']['value'], i.e. Fio column. - Handles: - - missing columnN - - columnN is None - - missing 'value' + SAFE ACCESSOR for Fio transaction column numbers. + + Fio JSON schema example: + "column5": { "name": "VS", "value": "123456" } + + But the structure is NOT guaranteed to exist. + So this function prevents KeyError or NoneType errors. + + Returns: + t["columnN"]["value"] or None """ key = f"column{n}" val = t.get(key) @@ -81,8 +134,8 @@ def safe_col(t: dict, n: int): def clean_date(dt_str: str): """ - Convert Fio date '2025-10-26+0200' -> '2025-10-26' - Fio spec: date is always rrrr-mm-dd+GMT. + Fio returns dates like: "2025-02-14+0100" + We strip timezone → "2025-02-14" """ if not dt_str: return None @@ -90,15 +143,19 @@ def clean_date(dt_str: str): def ensure_dir(path: Path): + """Creates directory if it doesn’t exist.""" path.mkdir(parents=True, exist_ok=True) def save_json_for_account(base_dir: str, account_cfg: dict, data: dict, d_from: date, d_to: date): """ - Uloží JSON do podsložky dle čísla účtu, název souboru podle období. + Saves raw JSON to: + //YYYY-MM-DD_to_YYYY-MM-DD.json + + Useful for debugging, backups, or re-imports. """ acc_num_raw = account_cfg["account_number"] - acc_folder_name = acc_num_raw.replace("/", "_") # 2101234567_2700 + acc_folder_name = acc_num_raw.replace("/", "_") # sanitize dir name for filesystem out_dir = Path(base_dir) / acc_folder_name ensure_dir(out_dir) @@ -113,28 +170,31 @@ def save_json_for_account(base_dir: str, account_cfg: dict, data: dict, d_from: # ========================================= -# MAIN IMPORT +# MAIN IMPORT LOGIC # ========================================= + def main(): start_all = time.time() - # období posledních 90 dní + # Calculate time range (last N days) today = date.today() d_from = today - timedelta(days=DAYS_BACK) d_to = today - print(f"=== Fio multi-account import ===") + print("=== Fio multi-account import ===") print(f"Období: {d_from} až {d_to}") print("Načítám účty z JSON konfigurace...") + # Load all accounts from accounts.json accounts = load_accounts(ACCOUNTS_FILE) print(f" Účtů v konfiguraci: {len(accounts)}\n") - # Připojení do DB + # Connect to database conn = pymysql.connect(**DB) cur = conn.cursor() - # SQL s ON DUPLICATE KEY UPDATE + # SQL INSERT with ON DUPLICATE KEY UPDATE + # This means: if transaction already exists (same unique key), update it. sql = """ INSERT INTO transactions ( @@ -174,6 +234,9 @@ def main(): total_inserted = 0 + # ====================================================== + # PROCESS EACH ACCOUNT IN accounts.json + # ====================================================== for acc in accounts: name = acc["name"] cfg_acc_num = acc["account_number"] @@ -182,17 +245,20 @@ def main(): print(f"--- Účet: {name} ({cfg_acc_num}) ---") t0 = time.time() + # --- 1) Download JSON from Fio API data = fetch_fio_json(token, d_from, d_to) if data is None: print(" Přeskakuji, žádná data / chyba API.\n") continue - # volitelné uložení JSON + # --- 2) Save raw JSON file to disk json_path = save_json_for_account(JSON_BASE_DIR, acc, data, d_from, d_to) print(f" JSON uložen do: {json_path}") - # extrakce transakcí + # --- 3) Extract transactions from JSON tree tlist = data["accountStatement"]["transactionList"].get("transaction", []) + + # FIO can return single transaction as an object (not list) if isinstance(tlist, dict): tlist = [tlist] @@ -202,13 +268,15 @@ def main(): print(" Žádné transakce, jdu dál.\n") continue + # FIO returns account ID under accountStatement.info.accountId fio_acc_id = data["accountStatement"]["info"]["accountId"] + + # Warn if account ID in JSON doesn't match config (informational only) if cfg_acc_num and cfg_acc_num.split("/")[0] not in fio_acc_id: - # jen varování, ne fatální chyba print(f" ⚠ Upozornění: accountId z Fio ({fio_acc_id}) " f"se neshoduje s account_number v konfiguraci ({cfg_acc_num})") - # připravit řádky pro batch insert + # --- 4) Build list of MySQL rows rows = [] for t in tlist: row = { @@ -245,25 +313,33 @@ def main(): } rows.append(row) - # batch insert + # --- 5) INSERT rows into MySQL in batches inserted = 0 + for i in range(0, len(rows), BATCH_SIZE): - chunk = rows[i:i + BATCH_SIZE] - cur.executemany(sql, chunk) + chunk = rows[i : i + BATCH_SIZE] + cur.executemany(sql, chunk) # fast multi-row insert/update conn.commit() inserted += len(chunk) elapsed = time.time() - t0 total_inserted += inserted + print(f" ✓ Zapsáno (insert/update): {inserted} řádků do DB za {elapsed:.2f} s\n") + # Close DB cur.close() conn.close() total_elapsed = time.time() - start_all + print(f"=== Hotovo. Celkem zapsáno {total_inserted} transakcí. " f"Celkový čas: {total_elapsed:.2f} s ===") +# ====================================================== +# ENTRY POINT +# ====================================================== + if __name__ == "__main__": main() diff --git a/30 Report.py b/30 Report.py new file mode 100644 index 0000000..5ab2b8f --- /dev/null +++ b/30 Report.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +FIO EXPORT SCRIPT — FULLY COMMENTED VERSION +------------------------------------------- + +This script connects to your MySQL "fio" database, +reads all transactions, and exports them into a highly formatted +Excel workbook. + +Excel file includes: + + • First sheet: "ALL" → contains ALL transactions + • Additional sheets: one for each account from accounts.json + • First 5 CZK sheets appear first in custom order + • All formatting exactly preserved (colors, borders, widths, formulas) + +Everything is generated automatically. +""" + +import mysql.connector +from mysql.connector import Error +from openpyxl import Workbook +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side +from datetime import datetime +import os +import glob +import json + +# ====================================================== +# CONFIGURATION +# ====================================================== + +# MySQL server parameters +DB_HOST = "192.168.1.76" +DB_PORT = 3307 +DB_USER = "root" +DB_PASS = "Vlado9674+" +DB_NAME = "fio" + +# Where to save Excel files +OUTPUT_DIR = r"Z:\Dropbox\!!!Days\Downloads Z230" + +# JSON file with list of accounts (name + account_number) +ACCOUNTS_JSON = r"C:\Users\vlado\PycharmProjects\FIO\accounts.json" + +# Columns that MUST be written as TEXT in Excel using ="value" +# to avoid Excel stripping zeros or changing formatting +TEXT_COLUMNS = ["cislo_uctu", "protiucet", "kod_banky", "ks", "vs", "ss"] + + +# ====================================================== +# REMOVE OLD EXPORT FILES +# ====================================================== + +def cleanup_old_exports(): + """ + Deletes older versions of exported XLSX files that match + specific filename patterns. This keeps your folder clean, + ensuring you only have the most recent export. + """ + patterns = [ + os.path.join(OUTPUT_DIR, "*FIO*transaction*.xlsx"), + os.path.join(OUTPUT_DIR, "*FIO*transactions*.xlsx"), + os.path.join(OUTPUT_DIR, "*FIO_transactions*.xlsx"), + ] + + # Check each pattern + for pattern in patterns: + for file in glob.glob(pattern): + try: + os.remove(file) + print(f"🗑 Deleted old export: {file}") + except: + # If file cannot be deleted (locked or permission denied), + # simply skip it. + pass + + +# ====================================================== +# CORE EXCEL FORMATTING FUNCTION +# ====================================================== + +def format_sheet(ws, rows, headers): + """ + Applies ALL formatting rules to a worksheet: + - Writes headers + - Writes all rows + - Converts selected columns to Excel text formulas + - Colors rows based on "objem" (red=negative, green=positive) + - Sets fixed column widths + - Adds borders to every cell + - Center-aligns first 10 columns + - Freezes header row and enables filtering + """ + + # ------------------------------- + # 1) Format HEADER row + # ------------------------------- + for col_idx in range(1, len(headers) + 1): + cell = ws.cell(row=1, column=col_idx) + cell.font = Font(bold=True) # bold text + cell.fill = PatternFill(start_color="FFFF00", fill_type="solid") # yellow background + + # ------------------------------- + # 2) Write DATA rows + # ------------------------------- + for row in rows: + excel_row = [] + for h in headers: + val = row[h] + + # For text-sensitive columns, write ="value" + # This prevents Excel from stripping zeros or treating them as numbers. + if h in TEXT_COLUMNS and val is not None: + excel_row.append(f'="{val}"') + else: + excel_row.append(val) + + ws.append(excel_row) + + # ------------------------------- + # 3) Background coloring by "objem" + # ------------------------------- + # Light red (ARGB) = negative + fill_red = PatternFill(start_color="FFFFDDDD", end_color="FFFFDDDD", fill_type="solid") + # Light green (ARGB) = positive or zero + fill_green = PatternFill(start_color="FFEEFFEE", end_color="FFEEFFEE", fill_type="solid") + + # Find column index where "objem" is located + objem_col_index = headers.index("objem") + 1 + + # Apply row coloring + for row_idx in range(2, len(rows) + 2): # Start at row 2 (row 1 = header) + cell_objem = ws.cell(row=row_idx, column=objem_col_index) + + # Convert objem to float + try: + value = float(cell_objem.value) + except: + value = 0 + + # Choose correct color + fill = fill_red if value < 0 else fill_green + + # Apply fill to entire row + for col_idx in range(1, len(headers) + 1): + ws.cell(row=row_idx, column=col_idx).fill = fill + + # ------------------------------- + # 4) Fixed column widths + # ------------------------------- + fixed_widths = [ + 6, 11, 11, 5, 14, 14, 8, 6, 13, 13, + 50, 53, 12, 12, 5, 49, 29, 5, 29, 16, + 15, 12, 49, 20 + ] + + # Apply width using A, B, C... column names + for i, width in enumerate(fixed_widths, start=1): + col_letter = chr(64 + i) # convert 1 → 'A', 2 → 'B', ... + ws.column_dimensions[col_letter].width = width + + # ------------------------------- + # 5) Add borders + alignment + # ------------------------------- + thin = Side(border_style="thin", color="000000") + border = Border(left=thin, right=thin, top=thin, bottom=thin) + align_center = Alignment(horizontal="center") + + total_rows = len(rows) + 1 + total_cols = len(headers) + + for row_idx in range(1, total_rows + 1): + for col_idx in range(1, total_cols + 1): + cell = ws.cell(row=row_idx, column=col_idx) + cell.border = border # add border + + # Center-align ONLY first 10 columns + if col_idx <= 10: + cell.alignment = align_center + + # Freeze header row so it stays visible while scrolling + ws.freeze_panes = "A2" + + # Enable auto filter on top row + ws.auto_filter.ref = ws.dimensions + + +# ====================================================== +# MAIN EXPORT PROCESS +# ====================================================== + +def export_fio(): + print("Connecting to MySQL...") + + # Connect to MySQL database + try: + conn = mysql.connector.connect( + host=DB_HOST, + port=DB_PORT, + user=DB_USER, + password=DB_PASS, + database=DB_NAME + ) + except Error as e: + print("❌ Failed to connect:", e) + return + + cur = conn.cursor(dictionary=True) + + # ------------------------------- + # Load accounts.json + # ------------------------------- + with open(ACCOUNTS_JSON, "r", encoding="utf-8") as f: + accounts = json.load(f) + + # ------------------------------- + # Define priority first sheets + # ------------------------------- + preferred_order = [ + "CZK rodina", + "CZK ordinace", + "CZK na jídlo", + "CZK TrialHelp", + "CZK maminka svojě věci" + ] + + accounts_sorted = [] + + # Step 1: add priority accounts first + for pref in preferred_order: + for acc in accounts: + if acc["name"] == pref: + accounts_sorted.append(acc) + + # Step 2: add remaining accounts afterward + for acc in accounts: + if acc not in accounts_sorted: + accounts_sorted.append(acc) + + # ------------------------------- + # Create a new Excel workbook + # ------------------------------- + wb = Workbook() + wb.remove(wb.active) # remove default empty sheet + + # ------------------------------- + # FIRST SHEET: ALL TRANSACTIONS + # ------------------------------- + cur.execute("SELECT * FROM transactions ORDER BY datum DESC") + all_rows = cur.fetchall() + + if all_rows: + headers = list(all_rows[0].keys()) + ws_all = wb.create_sheet(title="ALL") + ws_all.append(headers) + format_sheet(ws_all, all_rows, headers) + + # ------------------------------- + # INDIVIDUAL SHEETS PER ACCOUNT + # ------------------------------- + for acc in accounts_sorted: + acc_num = acc["account_number"] + sheet_name = acc["name"][:31] # Excel sheet name limit + + print(f"➡ Creating sheet: {sheet_name}") + + query = f""" + SELECT * + FROM transactions + WHERE cislo_uctu = '{acc_num}' + ORDER BY datum DESC + """ + + cur.execute(query) + rows = cur.fetchall() + + if not rows: + print(f"⚠ No data for {sheet_name}") + continue + + headers = list(rows[0].keys()) + ws = wb.create_sheet(title=sheet_name) + ws.append(headers) + + format_sheet(ws, rows, headers) + + conn.close() + + # ------------------------------- + # Save Excel file + # ------------------------------- + + cleanup_old_exports() + + # File name includes timestamp + timestamp = datetime.now().strftime("%Y-%m-%d %H-%M-%S") + filename = f"{timestamp} FIO transactions.xlsx" + output_file = os.path.join(OUTPUT_DIR, filename) + + wb.save(output_file) + + print(f"✅ Export complete:\n{output_file}") + + +# ====================================================== +# MAIN ENTRY POINT +# ====================================================== + +if __name__ == "__main__": + export_fio() diff --git a/31 Python.py b/31 Python.py new file mode 100644 index 0000000..6914849 --- /dev/null +++ b/31 Python.py @@ -0,0 +1,7 @@ +import os + +DROPBOX = os.path.join(os.path.expanduser("~"), "Dropbox") +SHARED_PATH = os.path.join(DROPBOX, "PycharmShared") +GLOBAL_ENV = os.path.join(SHARED_PATH, ".env") + +print(DROPBOX) \ No newline at end of file diff --git a/test_text.xlsx b/test_text.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..fd9c6500daed587652e4d9b962e9e84a139ad4bd GIT binary patch literal 4837 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 literal 0 HcmV?d00001