- Remove validation logic from calculate_tax_yearly_average() - Add _auto_download_missing_monthly_data() for silent auto-download - Fix duplicate validation code in CLI that caused unintended execution - Separate validation from calculation: --stats only calculates, --validate only validates - Maintain auto-download functionality for missing data in calculations - Ensure stats command shows only calculation results without validation output Root Cause: Validation code was embedded in tax calculation function and duplicated in CLI Solution: Extract validation from calculation, keep auto-download separate Result: --stats shows clean output, --validate provides full analysis Testing: ✅ Stats command clean, ✅ Validation command works, ✅ No type errors
590 lines
22 KiB
Python
Executable File
590 lines
22 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import sys
|
|
import os
|
|
import json
|
|
from datetime import datetime
|
|
|
|
# Přidání adresáře src do sys.path, aby bylo možné importovat moduly
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
|
|
|
|
import database
|
|
import data_fetcher
|
|
import holidays
|
|
import rate_finder
|
|
import rate_reporter
|
|
import data_validator
|
|
|
|
# Global debug flag
|
|
DEBUG = False
|
|
|
|
|
|
def debug_print(*args, **kwargs):
|
|
"""Print debug messages only if debug mode is enabled."""
|
|
if DEBUG:
|
|
print(*args, **kwargs)
|
|
|
|
|
|
def set_debug_mode(debug):
|
|
"""Set the debug mode for this module."""
|
|
global DEBUG
|
|
DEBUG = debug
|
|
|
|
# Nastavíme debug mód pro všechny moduly
|
|
database.set_debug_mode(DEBUG)
|
|
data_fetcher.set_debug_mode(DEBUG)
|
|
holidays.set_debug_mode(DEBUG)
|
|
rate_finder.set_debug_mode(DEBUG)
|
|
rate_reporter.set_debug_mode(DEBUG)
|
|
data_validator.set_debug_mode(DEBUG)
|
|
|
|
|
|
def format_single_rate_json(
|
|
currency, rate, requested_date, actual_date=None, fallback=False
|
|
):
|
|
"""Format single rate lookup as JSON."""
|
|
data = {
|
|
"currency": currency,
|
|
"rate": float(rate) if rate is not None else None,
|
|
"date": requested_date,
|
|
"timestamp": datetime.now().isoformat() + "Z",
|
|
}
|
|
if fallback and actual_date:
|
|
data["actual_date"] = actual_date
|
|
data["fallback"] = True
|
|
return data
|
|
|
|
|
|
def format_tax_rate_json(currency, year, rate, monthly_rates=None):
|
|
"""Format unified tax rate as JSON."""
|
|
data = {
|
|
"currency": currency,
|
|
"year": year,
|
|
"unified_rate": float(rate) if rate is not None else None,
|
|
"calculation_date": datetime.now().isoformat() + "Z",
|
|
}
|
|
if monthly_rates:
|
|
data["monthly_rates"] = monthly_rates
|
|
return data
|
|
|
|
|
|
def format_multi_year_json(currency, year_results):
|
|
"""Format multi-year stats as JSON."""
|
|
data = {
|
|
"currency": currency,
|
|
"results": [
|
|
{"year": year, "unified_rate": float(rate) if rate is not None else None}
|
|
for year, rate in year_results
|
|
],
|
|
"calculation_date": datetime.now().isoformat() + "Z",
|
|
}
|
|
return data
|
|
|
|
|
|
def format_last_rate_json(currency, rate, date):
|
|
"""Format last available rate as JSON."""
|
|
data = {
|
|
"currency": currency,
|
|
"rate": float(rate) if rate is not None else None,
|
|
"date": date,
|
|
"timestamp": datetime.now().isoformat() + "Z",
|
|
}
|
|
return data
|
|
|
|
|
|
def format_error_json(error_msg, error_code=None, details=None):
|
|
"""Format error response as JSON."""
|
|
data = {"error": error_msg, "timestamp": datetime.now().isoformat() + "Z"}
|
|
if error_code:
|
|
data["code"] = error_code
|
|
if details:
|
|
data["details"] = details
|
|
return data
|
|
|
|
|
|
def output_json(data):
|
|
"""Output data as formatted JSON."""
|
|
print(json.dumps(data, indent=2, ensure_ascii=False))
|
|
|
|
|
|
def check_and_update_yearly_data():
|
|
"""
|
|
Zkontroluje konzistenci ročních dat pro aktuální rok a případně je aktualizuje.
|
|
"""
|
|
current_year = datetime.now().year
|
|
debug_print(f"Kontroluji konzistenci ročních dat pro rok {current_year}...")
|
|
|
|
# Zkontrolujeme konzistenci dat
|
|
is_consistent = data_fetcher.check_yearly_data_consistency(
|
|
current_year, output_dir="data"
|
|
)
|
|
|
|
if not is_consistent:
|
|
debug_print(
|
|
f"Roční data pro rok {current_year} nejsou konzistentní. Stahuji aktualizovaná data..."
|
|
)
|
|
# Ujistěme se, že adresář data existuje
|
|
os.makedirs("data", exist_ok=True)
|
|
# Stáhneme roční data znovu
|
|
data_fetcher.download_yearly_data(current_year, output_dir="data", force=True)
|
|
else:
|
|
debug_print(f"Roční data pro rok {current_year} jsou aktuální.")
|
|
|
|
|
|
def main():
|
|
global DEBUG
|
|
|
|
# Inicializace databáze
|
|
database.init_db()
|
|
|
|
parser = argparse.ArgumentParser(description="Stahování a správa kurzů měn z ČNB.")
|
|
parser.add_argument(
|
|
"--year", type=int, help="Rok, pro který se mají stáhnout data (např. 2020)."
|
|
)
|
|
parser.add_argument(
|
|
"-c",
|
|
"--currency",
|
|
type=str,
|
|
help="Kód měny (např. USD) pro měsíční stahování, vyhledání kurzu nebo generování reportu.",
|
|
)
|
|
parser.add_argument(
|
|
"--start-date",
|
|
type=str,
|
|
help="Počáteční datum pro měsíční stahování nebo generování reportu ve formátu DD.MM.YYYY.",
|
|
)
|
|
parser.add_argument(
|
|
"--end-date",
|
|
type=str,
|
|
help="Koncové datum pro měsíční stahování nebo generování reportu ve formátu DD.MM.YYYY.",
|
|
)
|
|
parser.add_argument(
|
|
"--date",
|
|
type=str,
|
|
help="Datum pro stažení denních kurzů ve formátu DD.MM.YYYY.",
|
|
)
|
|
parser.add_argument(
|
|
"--get-rate",
|
|
"-d",
|
|
type=str,
|
|
help="Vyhledá kurz pro zadané datum. Formát: DD.MM.YYYY",
|
|
)
|
|
parser.add_argument(
|
|
"--auto-download",
|
|
action="store_true",
|
|
help="Automaticky stáhne denní data, pokud je po 14:30 a kurz pro dnešní datum není k dispozici.",
|
|
)
|
|
parser.add_argument(
|
|
"--report-year", type=int, help="Rok, pro který se má vygenerovat report kurzů."
|
|
)
|
|
parser.add_argument(
|
|
"--report-month",
|
|
type=int,
|
|
help="Měsíc, pro který se má vygenerovat report kurzů (1-12). Vyžaduje --report-year.",
|
|
)
|
|
parser.add_argument(
|
|
"--report-period",
|
|
nargs=2,
|
|
metavar=("START_DATE", "END_DATE"),
|
|
help="Období, pro které se má vygenerovat report kurzů. Formát: DD.MM.YYYY DD.MM.YYYY",
|
|
)
|
|
parser.add_argument(
|
|
"--stats",
|
|
nargs="?",
|
|
const=True,
|
|
type=int,
|
|
help="Vygeneruje 'Jednotný kurz' pro daňové účely podle metodiky ČNB. "
|
|
"Pokud je zadán rok, vytvoří kurz pro konkrétní rok. "
|
|
"Pokud není rok zadán, vytvoří kurzy pro všechny roky s dostupnými daty.",
|
|
)
|
|
parser.add_argument(
|
|
"--validate",
|
|
action="store_true",
|
|
help="Validuje data pro měnu nebo všechny měny. Zkontroluje konzistenci kurzů a detekuje možné chyby.",
|
|
)
|
|
parser.add_argument(
|
|
"--record-counts",
|
|
action="store_true",
|
|
help="Zobrazí počet záznamů podle časových období (týden, měsíc, čtvrtletí, pololetí, rok).",
|
|
)
|
|
parser.add_argument(
|
|
"--change-threshold",
|
|
type=float,
|
|
default=1.0,
|
|
help="Práh pro detekci změn kurzů v procentech (výchozí: 1.0).",
|
|
)
|
|
parser.add_argument(
|
|
"--gap-threshold",
|
|
type=int,
|
|
default=3,
|
|
help="Maximální přijatelná mezera v pracovních dnech (výchozí: 3).",
|
|
)
|
|
parser.add_argument(
|
|
"--debug", action="store_true", help="Zobrazí podrobné ladicí informace."
|
|
)
|
|
parser.add_argument(
|
|
"--json",
|
|
action="store_true",
|
|
help="Výstup ve formátu JSON místo prostého textu pro programové zpracování.",
|
|
)
|
|
parser.add_argument(
|
|
"--no-adaptive",
|
|
action="store_true",
|
|
help="Vypne adaptivní učení prahů na základě historických dat.",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Nastavíme debug mód
|
|
DEBUG = args.debug
|
|
set_debug_mode(DEBUG)
|
|
|
|
# Převedeme kód měny na velká písmena, pokud byl zadán
|
|
if args.currency:
|
|
args.currency = args.currency.upper()
|
|
|
|
# Kontrola a případná aktualizace ročních dat pro aktuální rok (pouze v debug módu)
|
|
if DEBUG:
|
|
check_and_update_yearly_data()
|
|
else:
|
|
# V normálním módu zkontrolujeme pouze při stahování dat
|
|
if (
|
|
args.year
|
|
or args.start_date
|
|
or args.end_date
|
|
or args.date
|
|
or args.get_rate
|
|
or args.report_year
|
|
or args.report_period
|
|
or args.stats
|
|
):
|
|
current_year = datetime.now().year
|
|
# Pro jednoduchost v normálním módu nebudeme kontrolovat konzistenci automaticky
|
|
pass
|
|
|
|
# Zde bude logika pro zpracování argumentů
|
|
# Zde bude logika pro zpracování argumentů
|
|
if args.validate:
|
|
# Validation command
|
|
base_threshold = args.change_threshold
|
|
adaptive = not args.no_adaptive
|
|
max_gap_days = getattr(args, "gap_threshold", 3) # Default to 3 if not defined
|
|
|
|
if args.currency:
|
|
# Validate specific currency
|
|
debug_print(f"Validuji data pro měnu {args.currency}...")
|
|
results = data_validator.validate_currency_data(
|
|
args.currency, args.year, base_threshold, adaptive, max_gap_days
|
|
)
|
|
|
|
if args.json:
|
|
output_json(results)
|
|
else:
|
|
text_output = data_validator.format_validation_text(results)
|
|
print(text_output)
|
|
else:
|
|
# Validate all currencies
|
|
debug_print("Validuji data pro všechny měny...")
|
|
results = data_validator.validate_all_currencies(
|
|
args.year, base_threshold, adaptive, max_gap_days
|
|
)
|
|
|
|
if args.json:
|
|
output_json(results)
|
|
else:
|
|
text_output = data_validator.format_validation_text(results)
|
|
print(text_output)
|
|
elif args.record_counts:
|
|
# Record counts command
|
|
if not args.currency:
|
|
print(
|
|
"Chyba: Pro --record-counts je nutné zadat měnu pomocí -c/--currency."
|
|
)
|
|
sys.exit(1)
|
|
|
|
debug_print(f"Získávám počty záznamů pro měnu {args.currency}...")
|
|
record_counts = data_validator.get_record_counts_by_period(
|
|
args.currency, args.year
|
|
)
|
|
|
|
if args.json:
|
|
output_json({"currency": args.currency, "record_counts": record_counts})
|
|
else:
|
|
print(f"Record Counts for {args.currency}:")
|
|
print("=" * 50)
|
|
|
|
for year_key, periods in record_counts.items():
|
|
print(f"\nYear {year_key}:")
|
|
print(f" Total records: {periods.get('year', 0)}")
|
|
|
|
# Half years
|
|
half_years = periods.get("half_year", {})
|
|
if half_years:
|
|
print(
|
|
f" Half years: H1={half_years.get('H1', 0)}, H2={half_years.get('H2', 0)}"
|
|
)
|
|
|
|
# Quarters
|
|
quarters = periods.get("quarter", {})
|
|
if quarters:
|
|
quarter_str = ", ".join(
|
|
[f"Q{q}={quarters.get(f'Q{q}', 0)}" for q in range(1, 5)]
|
|
)
|
|
print(f" Quarters: {quarter_str}")
|
|
|
|
# Months
|
|
months = periods.get("month", {})
|
|
if months:
|
|
month_list = []
|
|
for month in range(1, 13):
|
|
month_key = f"{month:02d}"
|
|
count = months.get(month_key, 0)
|
|
month_list.append(f"{month}={count}")
|
|
print(f" Months: {', '.join(month_list)}")
|
|
|
|
# Weeks summary
|
|
weeks = periods.get("week", {})
|
|
if weeks:
|
|
total_weeks = len(weeks)
|
|
if total_weeks <= 10:
|
|
week_list = sorted([f"{w}={weeks[w]}" for w in weeks.keys()])
|
|
print(f" Weeks: {', '.join(week_list)}")
|
|
else:
|
|
sample_weeks = sorted(list(weeks.keys())[:5])
|
|
week_sample = [f"{w}={weeks[w]}" for w in sample_weeks]
|
|
print(
|
|
f" Weeks: {', '.join(week_sample)}... ({total_weeks} total weeks)"
|
|
)
|
|
elif args.year:
|
|
# Validation command
|
|
base_threshold = args.change_threshold
|
|
adaptive = not args.no_adaptive
|
|
|
|
if args.currency:
|
|
# Validate specific currency
|
|
debug_print(f"Validuji data pro měnu {args.currency}...")
|
|
results = data_validator.validate_currency_data(
|
|
args.currency, args.year, base_threshold, adaptive
|
|
)
|
|
|
|
if args.json:
|
|
output_json(results)
|
|
else:
|
|
text_output = data_validator.format_validation_text(results)
|
|
print(text_output)
|
|
else:
|
|
# Validate all currencies
|
|
debug_print("Validuji data pro všechny měny...")
|
|
results = data_validator.validate_all_currencies(
|
|
args.year, base_threshold, adaptive
|
|
)
|
|
|
|
if args.json:
|
|
output_json(results)
|
|
else:
|
|
text_output = data_validator.format_validation_text(results)
|
|
print(text_output)
|
|
return
|
|
# elif args.currency and args.start_date and args.end_date and not args.report_period:
|
|
# Měsíční stahování dat
|
|
debug_print("HIT: Monthly download condition")
|
|
debug_print(
|
|
f"Stahuji měsíční data pro měnu {args.currency} od {args.start_date} do {args.end_date}..."
|
|
)
|
|
# Ujistěme se, že adresář data existuje
|
|
os.makedirs("data", exist_ok=True)
|
|
# Volání funkce pro stažení měsíčních dat
|
|
data_fetcher.download_monthly_data(
|
|
args.currency, args.start_date, args.end_date, output_dir="data"
|
|
)
|
|
elif args.report_period and args.currency:
|
|
start_date, end_date = args.report_period
|
|
debug_print("HIT: Report period condition")
|
|
debug_print(
|
|
f"Generuji report pro měnu {args.currency} od {start_date} do {end_date}..."
|
|
)
|
|
rate_reporter.generate_period_report(
|
|
start_date, end_date, args.currency, output_dir="data"
|
|
)
|
|
elif args.date:
|
|
debug_print("HIT: Daily data condition")
|
|
debug_print(f"Stahuji denní data pro datum {args.date}...")
|
|
# Ujistěme se, že adresář data existuje
|
|
os.makedirs("data", exist_ok=True)
|
|
# Volání funkce pro stažení denních dat
|
|
data_fetcher.download_daily_data(args.date, output_dir="data")
|
|
elif args.get_rate and args.currency:
|
|
debug_print("HIT: Get rate condition")
|
|
date_str = args.get_rate
|
|
currency_code = args.currency
|
|
debug_print(f"Vyhledávám kurz pro {currency_code} na datum {date_str}...")
|
|
rate = rate_finder.get_rate_for_date(date_str, currency_code)
|
|
if rate:
|
|
if args.json:
|
|
json_data = format_single_rate_json(currency_code, rate, date_str)
|
|
output_json(json_data)
|
|
else:
|
|
# Pro --get-rate v normálním režimu zobrazíme pouze kurz
|
|
if not DEBUG:
|
|
print(rate)
|
|
else:
|
|
print(
|
|
f"Kurz {currency_code} na datum {date_str} (nebo nejbližší pracovní den): {rate}"
|
|
)
|
|
else:
|
|
# Rate not found
|
|
if args.json:
|
|
error_data = format_error_json(
|
|
f"Kurz {currency_code} na datum {date_str} nebyl nalezen",
|
|
"RATE_NOT_FOUND",
|
|
)
|
|
output_json(error_data)
|
|
else:
|
|
if not DEBUG:
|
|
print("Kurz nenalezen")
|
|
else:
|
|
print(
|
|
f"Kurz {currency_code} na datum {date_str} (ani v předchozích dnech) nebyl nalezen."
|
|
)
|
|
elif args.get_rate is not None and not args.currency:
|
|
debug_print("HIT: Get rate without currency condition")
|
|
# Pokud je zadán --get-rate bez data a bez měny
|
|
if DEBUG:
|
|
print(
|
|
"Chyba: Pro použití --get-rate musí být zadána měna pomocí -c/--currency."
|
|
)
|
|
sys.exit(1)
|
|
# DŮLEŽITÉ: Pořadí následujících elif podmínek je důležité!
|
|
# Nejprve zpracujeme --stats, pak teprve "poslední dostupný kurz"
|
|
elif args.stats is not None and args.currency:
|
|
debug_print("HIT: Stats condition")
|
|
currency_code = args.currency
|
|
if args.stats is True:
|
|
# Pokud je --stats zadán bez roku, vytvoříme kurzy pro všechny roky s dostupnými daty
|
|
debug_print(
|
|
f"Generuji 'Jednotný kurz' pro daňové účely podle metodiky ČNB pro {currency_code} pro všechny roky..."
|
|
)
|
|
|
|
# Získáme seznam všech roků s daty
|
|
years = database.get_years_with_data()
|
|
if not years:
|
|
if not DEBUG:
|
|
print("Žádná data nenalezena")
|
|
else:
|
|
print("Databáze neobsahuje žádná data.")
|
|
return
|
|
|
|
# Pro každý rok vypočítáme 'Jednotný kurz'
|
|
year_results = []
|
|
for year in years:
|
|
# Zkontrolujeme, zda databáze obsahuje data pro daný rok
|
|
if not rate_finder.check_year_data_in_db(year):
|
|
debug_print(
|
|
f"Databáze neobsahuje data pro rok {year}. Stahuji roční data..."
|
|
)
|
|
# Ujistěme se, že adresář data existuje
|
|
os.makedirs("data", exist_ok=True)
|
|
# Stáhneme roční data s vynuceným stažením
|
|
data_fetcher.download_yearly_data(
|
|
year, output_dir="data", force=True
|
|
)
|
|
|
|
# Vypočítáme 'Jednotný kurz' podle metodiky ČNB
|
|
tax_rate = rate_reporter.calculate_tax_yearly_average(
|
|
year, currency_code, output_dir="data"
|
|
)
|
|
year_results.append((year, tax_rate))
|
|
|
|
if not args.json:
|
|
if tax_rate:
|
|
# Pro --stats v normálním režimu zobrazíme pouze 'Jednotný kurz' zaokrouhlený na 2 desetinná místa
|
|
if not DEBUG:
|
|
print(f"{year}: {tax_rate:.2f}")
|
|
else:
|
|
print(
|
|
f"'Jednotný kurz' pro daňové účely podle metodiky ČNB pro {currency_code} za rok {year}: {tax_rate:.2f}"
|
|
)
|
|
else:
|
|
if not DEBUG:
|
|
print(f"{year}: 'Jednotný kurz' nenalezen")
|
|
else:
|
|
print(
|
|
f"'Jednotný kurz' pro daňové účely podle metodiky ČNB pro {currency_code} za rok {year} nebyl nalezen."
|
|
)
|
|
|
|
# Output JSON for multi-year results
|
|
if args.json:
|
|
json_data = format_multi_year_json(currency_code, year_results)
|
|
output_json(json_data)
|
|
else:
|
|
# Pokud je --stats zadán s konkrétním rokem
|
|
year = args.stats
|
|
debug_print(
|
|
f"Generuji 'Jednotný kurz' pro daňové účely podle metodiky ČNB pro {currency_code} za rok {year}..."
|
|
)
|
|
|
|
# Zkontrolujeme, zda databáze obsahuje data pro daný rok
|
|
if not rate_finder.check_year_data_in_db(year):
|
|
debug_print(
|
|
f"Databáze neobsahuje data pro rok {year}. Stahuji roční data..."
|
|
)
|
|
# Ujistěme se, že adresář data existuje
|
|
os.makedirs("data", exist_ok=True)
|
|
# Stáhneme roční data s vynuceným stažením
|
|
data_fetcher.download_yearly_data(year, output_dir="data", force=True)
|
|
|
|
# Vypočítáme 'Jednotný kurz' podle metodiky ČNB
|
|
tax_rate = rate_reporter.calculate_tax_yearly_average(
|
|
year, currency_code, output_dir="data"
|
|
)
|
|
if args.json:
|
|
json_data = format_tax_rate_json(currency_code, year, tax_rate)
|
|
output_json(json_data)
|
|
else:
|
|
if tax_rate:
|
|
# Pro --stats v normálním režimu zobrazíme pouze 'Jednotný kurz' zaokrouhlený na 2 desetinná místa
|
|
if not DEBUG:
|
|
print(f"{tax_rate:.2f}")
|
|
else:
|
|
print(
|
|
f"'Jednotný kurz' pro daňové účely podle metodiky ČNB pro {currency_code} za rok {year}: {tax_rate:.2f}"
|
|
)
|
|
else:
|
|
if not DEBUG:
|
|
print("'Jednotný kurz' nenalezen")
|
|
else:
|
|
print(
|
|
f"'Jednotný kurz' pro daňové účely podle metodiky ČNB pro {currency_code} za rok {year} nebyl nalezen."
|
|
)
|
|
elif args.currency and not args.get_rate:
|
|
# Pokud je zadána měna, ale není zadán --get-rate, vytiskneme poslední dostupný kurz
|
|
# Toto musí být až po --stats, jinak by se --stats nikdy nevykonalo
|
|
currency_code = args.currency
|
|
debug_print(f"Vyhledávám poslední dostupný kurz pro {currency_code}...")
|
|
rate, date = database.get_last_rate_for_currency(currency_code)
|
|
if args.json:
|
|
json_data = format_last_rate_json(currency_code, rate, date)
|
|
output_json(json_data)
|
|
else:
|
|
if rate and date:
|
|
# Pro normální režim zobrazíme kurz ve formátu "11.11 # dated: dd.mm.yyyy"
|
|
if not DEBUG:
|
|
print(f"{rate} # dated: {date}")
|
|
else:
|
|
print(
|
|
f"Poslední dostupný kurz {currency_code}: {rate} # dated: {date}"
|
|
)
|
|
else:
|
|
if not DEBUG:
|
|
print("Kurz nenalezen")
|
|
else:
|
|
print(f"Poslední dostupný kurz pro {currency_code} nebyl nalezen.")
|
|
else:
|
|
if DEBUG:
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|