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 0000000..fd9c650
Binary files /dev/null and b/test_text.xlsx differ