diff --git a/README.md b/README.md index f86638d..0dc41b5 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@ Tento projekt je určen pro stahování a správu kurzů cizích měn vůči če - **Kontrola konzistence ročních dat**: Při startu programu automaticky kontroluje, zda roční data pro aktuální rok obsahují záznamy za poslední 3 pracovní dny. Pokud ne, data jsou automaticky aktualizována. - **Automatické stahování ročních dat**: Pokud jsou požadována data pro rok, který v databázi není, program automaticky stáhne roční data pro daný rok, aktualizuje databázi a teprve poté vrátí výsledek. - **Generování reportů**: Lze vygenerovat report kurzů pro zadaný rok, měsíc nebo časové období včetně dopočítaných kurzů pro dny, kdy ve vstupních datech neexistovali. -- **Správné dopočítání kurzů**: Program správně aplikuje pravidla ČNB pro dopočítání kurzů pro víkendy a svátky jak při vyhledávání (`--get-rate`), tak při generování reportů. + - **Správné dopočítání kurzů**: Program správně aplikuje pravidla ČNB pro dopočítání kurzů pro víkendy a svátky jak při vyhledávání (`--get-rate`), tak při generování reportů. + - **Výpočet Jednotného kurzu**: Lze vypočítat 'Jednotný kurz' pro daňové účely podle metodiky ČNB jako aritmetický průměr kurzů k posledním dnům každého měsíce v roce. + - **JSON výstup**: Všechny příkazy podporují JSON formát pro programové zpracování pomocí přepínače `--json`. ## Požadavky @@ -60,8 +62,10 @@ Při každém spuštění programu: - `--date DATUM`: Stáhne denní data pro zadané datum. Formát: `DD.MM.YYYY`. - `--get-rate DATUM`: Vyhledá kurz pro zadané datum. Formát: `DD.MM.YYYY`. Vyžaduje `-c` nebo `--currency`. - `--auto-download`: Povolí automatické stahování denních dat pro dnešní datum, pokud je po 14:30 a kurz není k dispozici. -- `--report-year ROK [--report-month MESIC]`: Vygeneruje report kurzů pro zadaný rok (a případně měsíc). Vyžaduje `-c` nebo `--currency`. -- `--report-period ZACATEK KONEC`: Vygeneruje report kurzů pro zadané časové období. Vyžaduje `-c` nebo `--currency`. + - `--report-year ROK [--report-month MESIC]`: Vygeneruje report kurzů pro zadaný rok (a případně měsíc). Vyžaduje `-c` nebo `--currency`. + - `--report-period ZACATEK KONEC`: Vygeneruje report kurzů pro zadané časové období. Vyžaduje `-c` nebo `--currency`. + - `--stats [ROK]`: Vypočítá '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. Vyžaduje `-c` nebo `--currency`. + - `--json`: Výstup ve formátu JSON místo prostého textu pro programové zpracování. ### Příklady @@ -111,6 +115,87 @@ Při každém spuštění programu: python src/cli.py --report-period 01.07.2020 31.07.2020 -c USD ``` +10. **Výpočet Jednotného kurzu pro daňové účely pro USD za rok 2025**: + ```bash + python src/cli.py --stats 2025 -c USD + ``` + +11. **Výpočet Jednotného kurzu pro daňové účely pro všechny roky s daty**: + ```bash + python src/cli.py --stats -c USD + ``` + +12. **Získání posledního dostupného kurzu USD**: + ```bash + python src/cli.py -c USD + ``` + +13. **JSON výstup pro vyhledání kurzu**: + ```bash + python src/cli.py --get-rate 01.01.2025 -c USD --json + ``` + +14. **JSON výstup pro výpočet Jednotného kurzu**: + ```bash + python src/cli.py --stats 2025 -c USD --json + ``` + +## JSON formát + +Při použití přepínače `--json` program vrací strukturovaná data ve formátu JSON: + +### Jednotlivý kurz +```json +{ + "currency": "USD", + "rate": 24.214, + "date": "01.01.2025", + "timestamp": "2025-01-12T10:30:00Z" +} +``` + +### Jednotný kurz pro jeden rok +```json +{ + "currency": "USD", + "year": 2025, + "unified_rate": 21.84, + "calculation_date": "2025-01-12T10:30:00Z" +} +``` + +### Jednotný kurz pro více let +```json +{ + "currency": "USD", + "results": [ + {"year": 2023, "unified_rate": 23.45}, + {"year": 2024, "unified_rate": 23.28}, + {"year": 2025, "unified_rate": 21.84} + ], + "calculation_date": "2025-01-12T10:30:00Z" +} +``` + +### Poslední dostupný kurz +```json +{ + "currency": "USD", + "rate": 20.632, + "date": "31.12.2025", + "timestamp": "2025-01-12T10:30:00Z" +} +``` + +### Chyba +```json +{ + "error": "Kurz nebyl nalezen", + "code": "RATE_NOT_FOUND", + "timestamp": "2025-01-12T10:30:00Z" +} +``` + ## Chování při různých časech a datumech - **Budoucí datum**: Program vrátí chybu, protože kurzy pro budoucí data ještě nebyly vydány. diff --git a/src/cli.py b/src/cli.py index b2f0f83..fd73ade 100755 --- a/src/cli.py +++ b/src/cli.py @@ -3,6 +3,7 @@ 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 @@ -17,16 +18,18 @@ import rate_reporter # 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) @@ -34,18 +37,91 @@ def set_debug_mode(debug): rate_finder.set_debug_mode(DEBUG) rate_reporter.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") - + 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...") + 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 @@ -53,81 +129,83 @@ def check_and_update_yearly_data(): 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)." + "--year", type=int, help="Rok, pro který se mají stáhnout data (např. 2020)." ) parser.add_argument( - "-c", "--currency", + "-c", + "--currency", type=str, - help="Kód měny (např. USD) pro měsíční stahování, vyhledání kurzu nebo generování reportu." + 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." + 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." + 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." + help="Datum pro stažení denních kurzů ve formátu DD.MM.YYYY.", ) parser.add_argument( - "--get-rate", "-d", + "--get-rate", + "-d", type=str, - help="Vyhledá kurz pro zadané datum. Formát: DD.MM.YYYY" + 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." + 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ů." + "--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." + 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" + 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='?', + 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." + "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( - "--debug", + "--debug", action="store_true", help="Zobrazí podrobné ladicí informace." + ) + parser.add_argument( + "--json", action="store_true", - help="Zobrazí podrobné ladicí informace." + help="Výstup ve formátu JSON místo prostého textu pro programové zpracování.", ) args = parser.parse_args() - + # Pokud nebyly zadány žádné argumenty, vytiskneme nápovědu a seznam dostupných měn if len(sys.argv) == 1: parser.print_help() @@ -138,25 +216,34 @@ def main(): else: print("Žádné měny nejsou v databázi k dispozici.") sys.exit(0) - + # 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: + 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ů if args.year: debug_print(f"Stahuji roční data pro rok {args.year}...") @@ -166,11 +253,23 @@ def main(): data_fetcher.download_yearly_data(args.year, output_dir="data") elif args.currency and args.start_date and args.end_date and not args.report_period: # Měsíční stahování dat - debug_print(f"Stahuji měsíční data pro měnu {args.currency} od {args.start_date} do {args.end_date}...") + 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") + 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( + 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(f"Stahuji denní data pro datum {args.date}...") # Ujistěme se, že adresář data existuje @@ -183,64 +282,38 @@ def main(): 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: - # Pro --get-rate v normálním režimu zobrazíme pouze kurz - if not DEBUG: - print(rate) + if args.json: + json_data = format_single_rate_json(currency_code, rate, date_str) + output_json(json_data) else: - print(f"Kurz {currency_code} na datum {date_str} (nebo nejbližší pracovní den): {rate}") + # 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: - # Pokud nebyl kurz nalezen a je aktivní přepínač --auto-download, zkusíme stáhnout denní data - if args.auto_download: - try: - requested_date = datetime.strptime(date_str, "%d.%m.%Y") - today = datetime.now() - - # Zkontrolujeme, zda je požadované datum dnešní - if requested_date.date() == today.date(): - # Zkontrolujeme, zda je čas po 14:30 - if today.time() >= datetime.strptime("14:30", "%H:%M").time(): - debug_print("Automaticky stahuji denní data...") - # Ujistěme se, že adresář data existuje - os.makedirs("data", exist_ok=True) - # Stáhneme denní data pro dnešní datum - today_str = today.strftime("%d.%m.%Y") - data_fetcher.download_daily_data(today_str, output_dir="data") - # Zkusíme znovu vyhledat kurz - rate = rate_finder.get_rate_for_date(date_str, currency_code) - if rate: - if not DEBUG: - print(rate) - else: - print(f"Kurz {currency_code} na datum {date_str} (nebo nejbližší pracovní den): {rate}") - 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 ani po stažení denních dat.") - else: - if not DEBUG: - print("Kurz nenalezen") - else: - print(f"Chyba: Automatické stahování nelze provést, protože čas ještě není po 14:30. Aktuální čas je {today.strftime('%H:%M')}.") - else: - if not DEBUG: - print("Kurz nenalezen") - else: - print("Automatické stahování denních dat je možné pouze pro dnešní datum.") - except ValueError: - if not DEBUG: - print("Kurz nenalezen") - else: - print(f"Neplatný formát data: {date_str}") + # 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.") + 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: # 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.") + 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" @@ -249,8 +322,10 @@ def main(): 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...") - + 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: @@ -259,77 +334,117 @@ def main(): 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...") + 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) - + 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 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}") + 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: - 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.") + 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}...") - + 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...") + 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 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}") + 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 not DEBUG: - print("'Jednotný kurz' nenalezen") + 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: - print(f"'Jednotný kurz' pro daňové účely podle metodiky ČNB pro {currency_code} za rok {year} nebyl nalezen.") + 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 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}") + if args.json: + json_data = format_last_rate_json(currency_code, rate, date) + output_json(json_data) else: - if not DEBUG: - print("Kurz nenalezen") + 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: - print(f"Poslední dostupný kurz pro {currency_code} nebyl nalezen.") + 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() \ No newline at end of file + main() diff --git a/src/rate_reporter.py b/src/rate_reporter.py index 0f5d631..4d86da6 100644 --- a/src/rate_reporter.py +++ b/src/rate_reporter.py @@ -1,5 +1,6 @@ import sys import os +import time from datetime import datetime, timedelta import calendar @@ -14,16 +15,53 @@ import rate_finder # 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 + +def get_czech_day_name(date_str): + """ + Vrátí český název dne v týdnu pro zadané datum. + + :param date_str: Datum ve formátu DD.MM.YYYY + :return: Český název dne v týdnu + """ + try: + date_obj = datetime.strptime(date_str, "%d.%m.%Y") + # Czech day names + czech_days = [ + "pondělí", + "úterý", + "středa", + "čtvrtek", + "pátek", + "sobota", + "neděle", + ] + return czech_days[date_obj.weekday()] + except ValueError: + return "neznámý den" + + +def is_holiday(date_str): + """ + Zkontroluje, zda je zadané datum státní svátek. + + :param date_str: Datum ve formátu DD.MM.YYYY + :return: True pokud je svátek, jinak False + """ + return holidays.is_holiday(date_str) + + def get_rate_for_date_with_fallback(date_str, currency_code): """ Vyhledá kurz pro zadané datum a měnu. Pokud kurz pro dané datum neexistuje, @@ -38,21 +76,21 @@ def get_rate_for_date_with_fallback(date_str, currency_code): requested_date = datetime.strptime(date_str, "%d.%m.%Y") except ValueError: return None - + # 1. Zkusíme najít kurz pro přesné datum rate = database.get_rate(date_str, currency_code) if rate is not None: return rate - + # 2. Pokud kurz neexistuje, aplikujeme pravidla podle ČNB: - # "Kurzy devizového trhu jsou vyhlašovány pro běžně obchodované měny, - # a to každý pracovní den po 14.30 s platností pro aktuální pracovní den + # "Kurzy devizového trhu jsou vyhlašovány pro běžně obchodované měny, + # a to každý pracovní den po 14.30 s platností pro aktuální pracovní den # a pro případnou následující sobotu, neděli či státní svátek" - # + # # To znamená: - # - Pro víkendy a svátky hledáme kurz zpět v čase podle pravidel ČNB + # - Pro víkendy a svátky hledáme kurz zpět v čase # - Pro běžné dny, které nemají kurz, hledáme kurz z posledního pracovního dne před nimi - + # Zkontrolujeme, zda je datum víkend nebo svátek if holidays.is_weekend(date_str) or holidays.is_holiday(date_str): # Pro víkendy a svátky hledáme kurz zpět v čase @@ -78,15 +116,208 @@ def get_rate_for_date_with_fallback(date_str, currency_code): for _ in range(10): current_date -= timedelta(days=1) check_date_str = current_date.strftime("%d.%m.%Y") - + # Zkontrolujeme, zda je to pracovní den - if not holidays.is_weekend(check_date_str) and not holidays.is_holiday(check_date_str): + if not holidays.is_weekend(check_date_str) and not holidays.is_holiday( + check_date_str + ): rate = database.get_rate(check_date_str, currency_code) if rate is not None: return rate - + return None + +def get_missing_months_for_tax_calculation(year, currency_code): + """ + Vrátí seznam měsíců, pro které chybí kurzy k posledním dnům pro výpočet 'Jednotného kurzu'. + Zahrnuje pouze měsíce, jejichž poslední den je v minulosti (lze stáhnout). + + :param year: Rok k ověření + :param currency_code: Kód měny + :return: Seznam měsíců (1-12) s chybějícími kurzy + """ + import calendar + + missing_months = [] + + for month in range(1, 13): + # Získáme poslední den měsíce + last_day = calendar.monthrange(year, month)[1] + date_str = f"{last_day:02d}.{month:02d}.{year}" + + # Pokud je datum v budoucnosti, přeskočíme (nelze stáhnout) + date_obj = datetime.strptime(date_str, "%d.%m.%Y") + current_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + if date_obj > current_date: + continue + + # Zkusíme najít kurz pro dané datum + rate = database.get_rate(date_str, currency_code) + if rate is None: + # Zkusíme dopočítat + calculated_rate = get_rate_for_date_with_fallback(date_str, currency_code) + if calculated_rate is None: + missing_months.append(month) + + return missing_months + + +def _is_year_complete_for_tax_calculation(year): + """ + Zkontroluje, zda je rok kompletní pro výpočet 'Jednotného kurzu' podle metodiky ČNB. + Rok je kompletní, pokud máme kurzy pro poslední den každého měsíce. + + :param year: Rok k ověření + :return: True pokud je rok kompletní, jinak False + """ + import calendar + + current_year = datetime.now().year + current_month = datetime.now().month + current_day = datetime.now().day + + # Pokud je rok v budoucnosti, není kompletní + if year > current_year: + return False + + # Pro všechny roky (včetně minulých) zkontrolujeme, zda máme data pro všechny měsíce + for month in range(1, 13): + # Získáme poslední den měsíce + last_day = calendar.monthrange(year, month)[1] + date_str = f"{last_day:02d}.{month:02d}.{year}" + + # Pokud je datum v budoucnosti, nemůže být kompletní + date_obj = datetime.strptime(date_str, "%d.%m.%Y") + current_date = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + if date_obj > current_date: + return False + + # Zkusíme najít kurz pro dané datum + rate = database.get_rate(date_str, "USD") # Testujeme na USD jako ukázku + if rate is None: + # Zkusíme dopočítat + calculated_rate = get_rate_for_date_with_fallback(date_str, "USD") + if calculated_rate is None: + return False + + return True + + # Pro aktuální rok zkontrolujeme, zda máme data pro všechny měsíce + for month in range(1, 13): + # Získáme poslední den měsíce + last_day = calendar.monthrange(year, month)[1] + date_str = f"{last_day:02d}.{month:02d}.{year}" + + # Pokud je měsíc v budoucnosti, nemůže být kompletní + if year == current_year and month > current_month: + return False + + # Zkusíme najít kurz pro dané datum + rate = database.get_rate(date_str, "USD") # Testujeme na USD jako ukázku + if rate is None: + # Zkusíme dopočítat + calculated_rate = get_rate_for_date_with_fallback(date_str, "USD") + if calculated_rate is None: + return False + + return True + + +def calculate_tax_yearly_average(year, currency_code, output_dir="data"): + """ + Vypočítá 'Jednotný kurz' pro daňové účely podle metodiky ČNB. + Jedná se o aritmetický průměr kurzů k posledním dnům každého kalendářního měsíce v roce. + + :param year: Rok + :param currency_code: Kód měny (např. USD) + :param output_dir: Adresář s daty + :return: 'Jednotný kurz' jako desetinné číslo, nebo None pokud není k dispozici + """ + debug_print( + f"Vypočítávám 'Jednotný kurz' pro daňové účely podle metodiky ČNB pro {currency_code} za rok {year}..." + ) + + # Zkusíme stáhnout chybějící měsíční data + missing_months = get_missing_months_for_tax_calculation(year, currency_code) + if missing_months: + debug_print( + f"Nalezeny chybějící měsíce pro rok {year}: {', '.join(f'{m:02d}' for m in missing_months)}. Stahuji měsíční data..." + ) + for month in missing_months: + start_date = f"01.{month:02d}.{year}" + last_day = calendar.monthrange(year, month)[1] + end_date = f"{last_day:02d}.{month:02d}.{year}" + debug_print( + f"Stahuji měsíční data pro {currency_code} za {month:02d}/{year}..." + ) + data_fetcher.download_monthly_data( + currency_code, start_date, end_date, output_dir="data" + ) + # Přidáme zpoždění, abychom nezatěžovali API + time.sleep(1) + + # Zkontrolujeme, zda je rok kompletní po stažení dat + if not _is_year_complete_for_tax_calculation(year): + debug_print( + f"Rok {year} není kompletní pro výpočet 'Jednotného kurzu'. Všechny měsíce musí mít dostupné kurzy k posledním dnům." + ) + return None + + # 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...") + # Stáhneme roční data s vynuceným stažením + os.makedirs("data", exist_ok=True) + data_fetcher.download_yearly_data(year, output_dir="data", force=True) + + # Získáme seznam posledních dní všech měsíců v roce + monthly_rates = [] + monthly_dates = [] + + debug_print(f"Získávám kurzy k posledním dnům všech měsíců v roce {year}...") + + for month in range(1, 13): + # Získáme poslední den měsíce + last_day = calendar.monthrange(year, month)[1] + date_str = f"{last_day:02d}.{month:02d}.{year}" + + # Získáme kurz pro dané datum + rate = database.get_rate(date_str, currency_code) + if rate is not None: + monthly_rates.append(rate) + monthly_dates.append(date_str) + debug_print(f"Měsíc {month:02d}: {date_str} = {rate}") + else: + # Kurz nebyl nalezen, zkusíme dopočítat + calculated_rate = get_rate_for_date_with_fallback(date_str, currency_code) + if calculated_rate is not None: + monthly_rates.append(calculated_rate) + monthly_dates.append(date_str) + debug_print( + f"Měsíc {month:02d}: {date_str} = {calculated_rate} (dopočítaný kurz)" + ) + else: + debug_print(f"Měsíc {month:02d}: {date_str} = kurz nenalezen") + + debug_print(f"Počet měsíců s kurzy: {len(monthly_rates)}/12") + + # Musíme mít kurzy pro všech 12 měsíců + if len(monthly_rates) != 12: + debug_print( + f"Varování: Nenalezeny kurzy pro všech 12 měsíců ({len(monthly_rates)}/12)" + ) + return None + + # Výpočet aritmetického průměru + average = sum(monthly_rates) / len(monthly_rates) + debug_print(f"Součet kurzů: {sum(monthly_rates):.6f}") + debug_print(f"Počet měsíců: {len(monthly_rates)}") + debug_print(f"'Jednotný kurz' pro daňové účely: {average:.6f}") + + return average + + def generate_yearly_report(year, currency_code, output_dir="data"): """ Vygeneruje report kurzů pro zadaný rok a měnu. @@ -97,59 +328,61 @@ def generate_yearly_report(year, currency_code, output_dir="data"): :return: Cesta k vytvořenému CSV souboru. """ debug_print(f"Generuji report kurzů pro {currency_code} za rok {year}...") - + # Zkontrolujeme, zda databáze obsahuje data pro daný rok # Pro generování reportu vždy stáhneme aktuální roční data, aby byl report kompletní debug_print(f"Stahuji roční data pro rok {year}...") # Stáhneme roční data s vynuceným stažením os.makedirs("data", exist_ok=True) data_fetcher.download_yearly_data(year, output_dir="data", force=True) - + # Vytvoříme seznam všech dní v roce start_date = datetime(year, 1, 1) end_date = datetime(year, 12, 31) - + # Ale nebudeme generovat report pro budoucí datumy today = datetime.now() # Pokud je požadovaný rok v budoucnosti, nepokračujeme if year > today.year: debug_print(f"Chyba: Nelze generovat report pro budoucí rok {year}.") return None - + # Pokud je požadovaný rok aktuální rok, omezíme konec na dnešní datum if year == today.year: end_date = today - + # Otevřeme CSV soubor pro zápis filename = f"report_{currency_code}_{year}.csv" filepath = os.path.join(output_dir, filename) - + try: - with open(filepath, 'w', newline='', encoding='utf-8') as csvfile: + with open(filepath, "w", newline="", encoding="utf-8") as csvfile: # Nové pořadí sloupců: Datum, Kurz, Den v týdnu, Svátek, Poznámka csvfile.write("Datum,Kurz,Den v týdnu,Svátek,Poznámka\n") - + current_date = start_date while current_date <= end_date: date_str = current_date.strftime("%d.%m.%Y") - + # Získáme den v týdnu day_name = get_czech_day_name(date_str) - + # Zkontrolujeme, zda je svátek is_holiday_flag = is_holiday(date_str) holiday_text = "ano" if is_holiday_flag else "ne" - + # Získáme kurz pro dané datum rate = database.get_rate(date_str, currency_code) note = "" - + if rate is not None: # Kurz byl nalezen v databázi pass else: # Kurz nebyl nalezen, zkusíme dopočítat - calculated_rate = get_rate_for_date_with_fallback(date_str, currency_code) + calculated_rate = get_rate_for_date_with_fallback( + date_str, currency_code + ) if calculated_rate is not None: rate = calculated_rate note = "Dopočítaný kurz" @@ -158,19 +391,22 @@ def generate_yearly_report(year, currency_code, output_dir="data"): note += " (svátek)" else: note = "Kurz není k dispozici" - + # Zapišeme řádek do CSV - csvfile.write(f"{date_str},{rate if rate is not None else ''},{day_name},{holiday_text},{note}\n") - + csvfile.write( + f"{date_str},{rate if rate is not None else ''},{day_name},{holiday_text},{note}\n" + ) + # Přejdeme na další den current_date += timedelta(days=1) - + debug_print(f"Report byl úspěšně uložen do: {filepath}") return filepath except IOError as e: debug_print(f"Chyba při zápisu do souboru: {e}") return None + def generate_monthly_report(year, month, currency_code, output_dir="data"): """ Vygeneruje report kurzů pro zadaný měsíc, rok a měnu. @@ -182,61 +418,63 @@ def generate_monthly_report(year, month, currency_code, output_dir="data"): :return: Chta k vytvořenému CSV souboru. """ debug_print(f"Generuji report kurzů pro {currency_code} za {month}/{year}...") - + # Zkontrolujeme, zda databáze obsahuje data pro daný rok # Pro generování reportu vždy stáhneme aktuální roční data, aby byl report kompletní debug_print(f"Stahuji roční data pro rok {year}...") # Stáhneme roční data s vynuceným stažením os.makedirs("data", exist_ok=True) data_fetcher.download_yearly_data(year, output_dir="data", force=True) - + # Vytvoříme seznam všech dní v měsíci start_date = datetime(year, month, 1) # Poslední den měsíce last_day = calendar.monthrange(year, month)[1] end_date = datetime(year, month, last_day) - + # Ale nebudeme generovat report pro budoucí datumy today = datetime.now() # Pokud je požadovaný měsíc v budoucnosti, nepokračujeme if year > today.year or (year == today.year and month > today.month): debug_print(f"Chyba: Nelze generovat report pro budoucí měsíc {month}/{year}.") return None - + # Pokud je požadovaný měsíc aktuální měsíc, omezíme konec na dnešní datum if year == today.year and month == today.month: end_date = today - + # Otevřeme CSV soubor pro zápis filename = f"report_{currency_code}_{year}_{month:02d}.csv" filepath = os.path.join(output_dir, filename) - + try: - with open(filepath, 'w', newline='', encoding='utf-8') as csvfile: + with open(filepath, "w", newline="", encoding="utf-8") as csvfile: # Nové pořadí sloupců: Datum, Kurz, Den v týdnu, Svátek, Poznámka csvfile.write("Datum,Kurz,Den v týdnu,Svátek,Poznámka\n") - + current_date = start_date while current_date <= end_date: date_str = current_date.strftime("%d.%m.%Y") - + # Získáme den v týdnu day_name = get_czech_day_name(date_str) - + # Zkontrolujeme, zda je svátek is_holiday_flag = is_holiday(date_str) holiday_text = "ano" if is_holiday_flag else "ne" - + # Získáme kurz pro dané datum rate = database.get_rate(date_str, currency_code) note = "" - + if rate is not None: # Kurz byl nalezen v databázi pass else: # Kurz nebyl nalezen, zkusíme dopočítat - calculated_rate = get_rate_for_date_with_fallback(date_str, currency_code) + calculated_rate = get_rate_for_date_with_fallback( + date_str, currency_code + ) if calculated_rate is not None: rate = calculated_rate note = "Dopočítaný kurz" @@ -245,20 +483,25 @@ def generate_monthly_report(year, month, currency_code, output_dir="data"): note += " (svátek)" else: note = "Kurz není k dispozici" - + # Zapišeme řádek do CSV - csvfile.write(f"{date_str},{rate if rate is not None else ''},{day_name},{holiday_text},{note}\n") - + csvfile.write( + f"{date_str},{rate if rate is not None else ''},{day_name},{holiday_text},{note}\n" + ) + # Přejdeme na další den current_date += timedelta(days=1) - + debug_print(f"Report byl úspěšně uložen do: {filepath}") return filepath except IOError as e: debug_print(f"Chyba při zápisu do souboru: {e}") return None -def generate_period_report(start_date_str, end_date_str, currency_code, output_dir="data"): + +def generate_period_report( + start_date_str, end_date_str, currency_code, output_dir="data" +): """ Vygeneruje report kurzů pro zadané období a měnu. @@ -268,68 +511,76 @@ def generate_period_report(start_date_str, end_date_str, currency_code, output_d :param output_dir: Adresář, kam se má CSV soubor s reportem uložit. :return: Chta k vytvořenému CSV souboru. """ - debug_print(f"Generuji report kurzů pro {currency_code} za období {start_date_str} - {end_date_str}...") - + debug_print( + f"Generuji report kurzů pro {currency_code} za období {start_date_str} - {end_date_str}..." + ) + try: start_date = datetime.strptime(start_date_str, "%d.%m.%Y") end_date = datetime.strptime(end_date_str, "%d.%m.%Y") except ValueError as e: debug_print(f"Neplatný formát data: {e}") return None - + # Ale nebudeme generovat report pro budoucí datumy today = datetime.now() # Pokud je požadované období v budoucnosti, nepokračujeme if start_date.date() > today.date(): - debug_print(f"Chyba: Nelze generovat report pro období v budoucnosti (od {start_date_str}).") + debug_print( + f"Chyba: Nelze generovat report pro období v budoucnosti (od {start_date_str})." + ) return None - + # Pokud je konec období v budoucnosti, omezíme ho na dnešní datum if end_date.date() > today.date(): - debug_print(f"Upozornění: Konec období byl omezen na dnešní datum ({today.strftime('%d.%m.%Y')}), protože zbytek je v budoucnosti.") + debug_print( + f"Upozornění: Konec období byl omezen na dnešní datum ({today.strftime('%d.%m.%Y')}), protože zbytek je v budoucnosti." + ) end_date = today - + # Zkontrolujeme, zda databáze obsahuje data pro roky v rozmezí # Pro generování reportu vždy stáhneme aktuální roční data, aby byl report kompletní start_year = start_date.year end_year = end_date.year - + for year in range(start_year, end_year + 1): debug_print(f"Stahuji roční data pro rok {year}...") # Stáhneme roční data s vynuceným stažením os.makedirs("data", exist_ok=True) - data_fetcher.download_yearly_data(year, output_dir="data", force=True) - + data_fetcher.download_yearly_data(year, output_dir="data") + # Otevřeme CSV soubor pro zápis filename = f"report_{currency_code}_{start_date_str.replace('.', '_')}_to_{end_date_str.replace('.', '_')}.csv" filepath = os.path.join(output_dir, filename) - + try: - with open(filepath, 'w', newline='', encoding='utf-8') as csvfile: + with open(filepath, "w", newline="", encoding="utf-8") as csvfile: # Nové pořadí sloupců: Datum, Kurz, Den v týdnu, Svátek, Poznámka csvfile.write("Datum,Kurz,Den v týdnu,Svátek,Poznámka\n") - + current_date = start_date while current_date <= end_date: date_str = current_date.strftime("%d.%m.%Y") - + # Získáme den v týdnu day_name = get_czech_day_name(date_str) - + # Zkontrolujeme, zda je svátek is_holiday_flag = is_holiday(date_str) holiday_text = "ano" if is_holiday_flag else "ne" - + # Získáme kurz pro dané datum rate = database.get_rate(date_str, currency_code) note = "" - + if rate is not None: # Kurz byl nalezen v databázi pass else: # Kurz nebyl nalezen, zkusíme dopočítat - calculated_rate = get_rate_for_date_with_fallback(date_str, currency_code) + calculated_rate = get_rate_for_date_with_fallback( + date_str, currency_code + ) if calculated_rate is not None: rate = calculated_rate note = "Dopočítaný kurz" @@ -338,49 +589,28 @@ def generate_period_report(start_date_str, end_date_str, currency_code, output_d note += " (svátek)" else: note = "Kurz není k dispozici" - + # Zapišeme řádek do CSV - csvfile.write(f"{date_str},{rate if rate is not None else ''},{day_name},{holiday_text},{note}\n") - + csvfile.write( + f"{date_str},{rate if rate is not None else ''},{day_name},{holiday_text},{note}\n" + ) + # Přejdeme na další den current_date += timedelta(days=1) - + debug_print(f"Report byl úspěšně uložen do: {filepath}") return filepath except IOError as e: debug_print(f"Chyba při zápisu do souboru: {e}") return None -def get_czech_day_name(date_str): - """ - Vrátí český název dne v týdnu pro zadané datum. - - :param date_str: Datum ve formátu DD.MM.YYYY - :return: Český název dne v týdnu - """ - try: - date_obj = datetime.strptime(date_str, "%d.%m.%Y") - # Czech day names - czech_days = ["pondělí", "úterý", "středa", "čtvrtek", "pátek", "sobota", "neděle"] - return czech_days[date_obj.weekday()] - except ValueError: - return "neznámý den" - -def is_holiday(date_str): - """ - Zkontroluje, zda je zadané datum státní svátek. - - :param date_str: Datum ve formátu DD.MM.YYYY - :return: True pokud je svátek, jinak False - """ - return holidays.is_holiday(date_str) # Příklad použití if __name__ == "__main__": # Inicializace databáze (pro případ spuštění samostatně) database.init_db() - + # Test # generate_yearly_report(2020, "USD") # generate_monthly_report(2020, 7, "USD") - # generate_period_report("01.07.2020", "31.07.2020", "USD") \ No newline at end of file + # generate_period_report("01.07.2020", "31.07.2020", "USD")