o
    Mj                    @   s	  U d Z ddlZddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddlZddlmZmZ ddlmZ ddlmZ ddlmZmZmZmZmZmZmZmZ ddlmZ ddlmZ ej !dej "e# ddl$Z%ddl&Z&e&'  ee(Z)ee)j*d	d	d	d	d
e)_*e+de)_,de)j-d< de)j-d< de)j-d< e)j.dd Z/e)0ddd Z1ej2ej3dd e4dZ5ddl6m7Z8 ee#9 j:d Z;e;j<dd e;d Z=e4 Z>e>?ej@ eAdd e>jBD ZCeCse8e=d d!d"d#d$ZDeD?ej@ eDEejFd%d&d' e>GeD e4dHd(e= d) e4d*ZId.d,eJd-eJd.dfd/d0ZKee#j:ZLeLd1 ZMeMd2 ZNeLd3 ZOd.ePfd4d5ZQd6ePd.dfd7d8ZRd9ZSd:d;d<d=d>d?ZTeUd@ZVd.eWfdAdBZXdCZYd.eJfdDdEZZi Z[ePeJe\e] f e^dF< e	_ Z`dGeJdHeadIead.eWfdJdKZbd/dMeafdNdOZcd.eJdB fdPdQZddRdS ZedTeJfdUdVZfdWZgdXZhi Zie	_ Zjd0dYdZZkd[eJd\eJd.eJfd]d^Zld_eJdB fd`daZmd_eJdB d.dfdbdcZnd_eJfdddeZodfdg Zpd1dieJdjeJd.eJfdkdlZqdmeJd.eJfdndoZrdpZsdqefdrdsZtdqefdtduZudqefdvdwZvdqefdxdyZwdqefdzd{Zxdqefd|d}ZyeUd~ejzZ{de\d.e|fddZ}deJdePd.dfddZ~deJd.ePfddZdZd2deJdmeJd.efddZG dd dejZddlZdd Zd.eJfddZe)de)ddd ZdddddddZe)jddgddd Ze)jddgddd Ze)jddgddd Ze)jddgddd Ze)jddgdeedd Ze)jddgdeedd Ze)jddgdeedd ZdZdddddddddddddZddÄ Ze)jddgdeeddƄ Ze)jddgdeeddɄ Ze)jddgdeedd̄ Ze)jddgdeeddτ Ze)dСeedd҄ Ze)dӡeeddՄ Ze)d֡eedd؄ Ze)d١e)dڡdd܄ Ze)jddgddd߄ Ze)jddgddd Ze)jddgddd Ze)jddgdepdd Ze)jddgdepdd Ze)jddgdepdd Ze)jddgdepdd Ze)jddgdepdd Ze)jddgdepdd Ze)jddgdepdd Ze)jddgdepdd Ze)jddgdepdd Ze)jd dgdepdd Ze)ddd Zee#j:d Ze)dd	d
 Ze)ddd Ze)ddd Ze)ddd Ze)ddd Ze)jddgddd Ze)jddgdepdd Ze)jddgdepdd Ze)jddgdepd d! Ze)jd"dgdeed#d$ Ze)jd%dgdepd&d' Ze)d(d)d* Ze)d+d,d- ZdS (3  u   
EEG-Assistent  –  Version 1 (Upload & ZIP-Download)
Keine persistenten Daten am Server. Jeder Nutzer bekommt eine UUID-Session
mit eigenem Temp-Verzeichnis. Nach dem ZIP-Download wird alles gelöscht.
    N)datetime	timedeltawraps)Path)FlaskResponsejsonifyredirectrender_templaterequest	send_filemake_responsesecure_filename)ProxyFix   )x_forx_protox_hostx_prefix   i  @MAX_CONTENT_LENGTH:	 SEND_FILE_MAX_AGE_DEFAULTTTEMPLATES_AUTO_RELOADc                 C   s    t jdrd| j_d| j_| S )uP   Cache-Control für statische Dateien setzen; SSE-Streams explizit ausschließen.z/static/r   T)r   path
startswithcache_controlmax_agepublic)response r"   X/sessions/vibrant-dreamy-cray/mnt/energie-durchdacht/EEG-Abrechnung/v1.07b_upload/app.py_add_cache_headers1   s   r$     c                 C   s   t ddidfS )Nerroru,   Upload zu groß (Limit: 100 MB pro Request).r%   )r	   )_er"   r"   r#   
_too_large:   s   r(   z'%(asctime)s %(levelname)-8s %(message)s)levelformatv1)TimedRotatingFileHandlerlogsexist_okfull.logc                 c   s,    | ]}t |tot|d ddV  qdS )baseFilename r0   N)
isinstance_TRFHgetattrendswith.0hr"   r"   r#   	<genexpr>O   s
    
r:   midnight   utf-8F)whenbackupCountencodingutczB%(asctime)s %(levelname)-5s %(name)s [%(threadName)s]: %(message)sz%Y-%m-%d %H:%M:%S)datefmtzFull-Log aktiv: z! (DEBUG-Level, 30 Tage Retention)sseINFOmessager)   returnc                 C   sT   t jt jt jt jt jd}zt|t	|
 t j|  W dS  ty)   Y dS w )zKSpiegelt eine SSE-Zeile in den Python-Logger, damit sie in full.log landet.)DEBUGrD   WARNINGERRORCRITICALN)loggingrG   rD   rH   rI   rJ   _sse_logloggetstrupper	Exception)rE   r)   	level_mapr"   r"   r#   _mirror_emitc   s   $rS   sepa_templatesRechnungsvorlage_Standard.xlsxzsmtp_config.jsonc               	   C   s   i } t  rztt jdd} W n	 ty   Y nw | dp&tjdd}| dp2tjdd| dp=tjd	d| d
pHtjdd|| dpTtjdd| dpatjddpa|dS )zHLiest SMTP-Config aus smtp_config.json; Fallback auf Umgebungsvariablen.r=   r@   usernameEEG_SMTP_USERNAMEr2   serverEEG_SMTP_SERVERportEEG_SMTP_PORTmodeEEG_SMTP_MODEsslpasswordEEG_SMTP_PASSWORDfrom_addressEEG_SMTP_FROMrY   r[   r]   rW   r`   rb   )	SMTP_CONFIG_FILEexistsjsonloads	read_textrQ   rN   osenviron)cfgrW   r"   r"   r#   _smtp_cfg_read|   s   rm   datac                 C   s   t jtj| ddddd dS )z*Speichert SMTP-Config in smtp_config.json.F   ensure_asciiindentr=   rV   N)re   
write_textrg   dumpsrn   r"   r"   r#   _smtp_cfg_write   s   
rv   uO  File Mitgliederliste;;
Bezeichnung vom Tabellenblatt für die Mitgliederverwaltung;TB_Mitglied;Mitglieder
Nummer vom Mitglied;Mitglied_ID;ID
Vorname vom Mitglied;Mitglied_Vorname;Vorname
Nachname vom Mitglied;Mitglied_Nachname;Nachname
Geschlecht vom Mitglied;Mitglied_Sex;Sex
Strasse vom Mitglied;Mitglied_Adresse;Adresse
Postleitzahl vom Mitglied;Mitglied_PLZ;PLZ
Wohnort vom Mitglied;Mitglied_Ort;Ort
E-Mail-Adresse vom Mitglied;Mitglied_Mail;Mail
IBAN vom Konto des Mitglieds;Mitglied_IBAN;IBAN
BIC vom Konto des Mitglieds;Mitglied_BIC;BIC
Firmenname vom Mitglied für die Rechnungslegung;Mitglied_Firmenname;Rechnungsname
Zählpunktnummer vom Mitglied für den Bezug;Mitglied_CONSUMPTION;CONSUMPTION
Zählpunktnummer vom Mitglied für die Einspeisung;Mitglied_GENERATION;GENERATION
Steuersatz vom Mitglied (0/20/13 – leer = 0);Mitglied_Steuersatz;Steuersatz
UID-Nummer vom Mitglied (optional, Format ATU + 8 Ziffern);Mitglied_UID;UID_Nummer
;;
EDA Report-FILE;;
Bezeichnung vom Tabellenblatt mit der Übersicht im EDA-Report;TB_EDA;Übersicht
Schlagwort im EDA-Report um die Kopfzeile für die Abrechnung zu finden;EDA_Header;Zählpunkt
Energierichtung Bezug;EDA_direction_C;CONSUMPTION
Energierichtung Einspeisung;EDA_direction_G;GENERATION
Überschrift Zählpunkt;EDA_ZP;Zählpunkt
Überschrift Energierichtung;EDA_direction;Energierichtung
Überschrift Zeitraum von;EDA_time_from;Zeitraum von
Überschrift Zeitraum bis;EDA_time_to;Zeitraum bis
Überschrift Eigendeckung;EDA_Eigendeckung;Eigendeckung gemeinschaftliche Erzeugung [KWH]
Überschrift Erzeugung;EDA_Erzeugung;Erzeugung lt. Messung entsprechend dem Teilnahmefaktor und EC-ID [KWH]
Überschrift Restüberschuss;EDA_Overflow;Restüberschuss bei EG und je ZP [KWH]
Überschrift Gesamterzeugung;EDA_Gesamterzeugung;Gesamte gemeinschaftliche Erzeugung [KWH]
Überschrift Gesamtverbrauch;EDA_Gesamtverbrauch;Gesamtverbrauch lt. Messung (bei Teilnahme gem. Erzeugung) [KWH]
;;
Buchhaltung;;
Bezeichnung vom Tabellenblatt für Rechnugnsnummern;TB_Rechnungsnummer;Rechnungsnummern
;;
Rechnungsvorlage;;
Bezeichnung vom Tabellenblatt für Vorlage;TB_Vorlage;Vorlage
u$   Rechnung {Periode} – {Vereinsname}LieberLiebezLiebe/ru   {Anrede} {Name},

im Anhang befindet sich Ihre Rechnung von {Vereinsname} für die Periode {Periode}.
Der Betrag von {Betrag} € wird am {Datum} {Aktion}.

Bei Fragen stehen wir gerne zur Verfügung.

Freundliche Grüße,
{Vereinsname})betreffanrede_manrede_wanrede_defaultbodyz>^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$c                 C   s   t | tott| S N)r3   rO   bool_SID_REmatchsidr"   r"   r#   
_valid_sid   s   r   license_sessionc                  C   s.   t jdd} | r| dd  S t jpdS )z@Holt die Client-IP, auch hinter Reverse-Proxy (X-Forwarded-For).zX-Forwarded-Forr2   ,r   )r   headersrN   splitstripremote_addr)fwdr"   r"   r#   
_client_ip   s   
r   _RATE_BUCKETSkey	max_callswindow_sc                    s   | sdS t   }||  tX t| g } fdd|D }t||kr1|t| < 	 W d   dS || |t| < ttdkrV fddt D }|D ]}t|d qMW d   dS W d   dS 1 siw   Y  dS )u   Gibt True zurück, wenn der Aufruf erlaubt ist, sonst False.
    Implementiert als Sliding-Window (Liste von Timestamps pro Key).Tc                    s   g | ]}| kr|qS r"   r"   )r8   tcutoffr"   r#   
<listcomp>       z_rate_limit.<locals>.<listcomp>NFi'  c                    s$   g | ]\}}|r|d   k r|qS )r"   )r8   kvr   r"   r#   r        $ )time_RATE_BUCKETS_LOCKr   rN   lenappenditemspop)r   r   r   nowtimesdeadr   r"   r   r#   _rate_limit   s2   


r   <   retry_afterc                 C   s&   t ddd}d|_t| |jd< |S )Nz%Zu viele Anfragen. Bitte kurz warten.rate_limitedr&   reason  zRetry-After)r	   status_coderO   r   )r   respr"   r"   r#   _rate_limit_response  s   r   c                   C   s   t jtS r~   )r   cookiesrN   LICENSE_COOKIEr"   r"   r"   r#   _license_session_id  s   r   c                       t   fdd}|S )u   Endpoint-Schutz: Abbruch mit 401 wenn keine gültige Lizenz-Session.
    Hängt die Kontodaten als `request.license` an (dict aus licensing).c                     sZ   t  }|r
t|nd }|d u r#tddd}d|_|jtdd |S |t_ | i |S )NzKein Zugangscode aktiv.Tr&   
need_login  /r   )	r   	licensingget_sessionr	   r   delete_cookier   r   license)argskwargsr   infor   fnr"   r#   wrapper  s   z require_license.<locals>.wrapperr   r   r   r"   r   r#   require_license  s   r   
session_idc              	   C   s$   | j t|ddttjtjdd | S )u   Setzt den License-Cookie mit sicheren Defaults.
    `secure` wird automatisch auf True gesetzt, wenn über HTTPS angefragt wurde.TLaxr   httponlysamesitesecurer   r   )
set_cookier   r   r   	is_securer   SESSION_TTL_SECONDS)r   r   r"   r"   r#   _set_license_cookie+  s   	r   admin_sessionip  c                     s^   t   t  fddt D } | D ]}t|d qW d   dS 1 s(w   Y  dS )zBEntfernt abgelaufene Admin-Sessions (bei jedem Access aufgerufen).c                    s    g | ]\}}|d   k r|qS )expiresr"   )r8   r   sr   r"   r#   r   L  s     z_admin_gc.<locals>.<listcomp>N)r   r   _ADMIN_SESSIONS_LOCK_ADMIN_SESSIONSr   r   )r   r   r"   r   r#   	_admin_gcH  s   "r   rW   ipc                 C   s\   t  jt  j }t | t ttd |dt|< W d   |S 1 s'w   Y  |S )u;   Legt eine neue Admin-Session an und gibt das Token zurück.seconds)rW   r   r   N)	uuiduuid4hexr   r   r   r   ADMIN_SESSION_TTLr   )rW   r   tokenr"   r"   r#   _admin_create_sessionQ  s   
r   r   c                 C   s   | sdS t   t2 t| }|r-|d t kr-t ttd |d< |W  d   S t| d W d   dS 1 s>w   Y  dS )u;   Gibt die Admin-Session zurück (verlängert TTL) oder None.Nr   r   )	r   r   r   rN   r   r   r   r   r   )r   r   r"   r"   r#   _admin_session_for]  s   

r   c                 C   s>   | sd S t  t| d  W d    d S 1 sw   Y  d S r~   )r   r   r   )r   r"   r"   r#   _admin_destroy_sessionk  s
   "r   c              	   C   s"   | j t|ddttjtdd | S )NTStrict/adminr   )r   ADMIN_COOKIEr   r   r   r   )r   r   r"   r"   r#   _set_admin_cookier  s   	r   c                    r   )uJ   Endpoint-Schutz für das Admin-Panel. 401 + need_login wenn keine Session.c                     sZ   t jt}t|}|d u r!tddd}d|_|jtdd |S |d t _ | i |S )NzAdmin-Login erforderlich.Tr   r   r   r   rW   )	r   r   rN   r   r   r	   r   r   
admin_user)r   r   tokr   r   r   r"   r#   r     s   
zrequire_admin.<locals>.wrapperr   r   r"   r   r#   require_admin  s   r   
upload.binnamedefaultc                 C   s   t | pd}|r
|S |S )u   secure_filename entfernt Pfad-Trenner und gefährliche Zeichen.
    Fällt auf `default` zurück, wenn danach nichts übrig bleibt (z.B. '../').r2   r   )r   r   r   r"   r"   r#   _safe_filename  s   r   periodec                 C   s,   t ddt| pd }|d}|pdS )u   Periode darf als Ordnername und in Dateinamen erscheinen – hier auf
    ASCII-Wortzeichen + '-' + '_' reduzieren. Leere Werte werden gefiltert,
    damit kein '..' oder Separator-Missbrauch möglich ist.z[^A-Za-z0-9._-]+_r2   z._-
Abrechnung)resubrO   r   )r   r   r"   r"   r#   _safe_periode  s   
r   ).xlsx.xlsmuploadsc                 C   s~   |   sdS t|  D ]}|j tv r d|j v r |  S qt|  D ]}|j tv r<|j ds<|  S q'dS )zFindet die Mitglieder-Datei im uploads/-Ordner (xlsx/xlsm mit 'mitglied' im Namen).
    Fallback: erste xlsx/xlsm ohne 'rc' am Anfang (RC = EDA-Report).Nmitgliedrc)is_dirsortediterdirsuffixlower_EXCEL_EXTSr   r   r   fr"   r"   r#   _find_mitglieder_file  s   r   c                 C   sF   |   sdS t|  D ]}|j dv r d|j v r |  S qdS )zLFindet eine hochgeladene Rechnungsvorlage (xlsx/xls mit 'vorlage' im Namen).Nr   z.xlsvorlager   r   r   r   r   r   r   r"   r"   r#   _find_vorlage_file     r   c                 C   F   |   sdS t|  D ]}|j dkr d|j v r |  S qdS )z(Findet eine hochgeladene Parameter.json.N.json	parameterr   r   r"   r"   r#   _find_param_file  r  r  c                 C   r  )z$Findet eine hochgeladene Config.csv.Nz.csvconfigr   r   r"   r"   r#   _find_config_file  r  r  c                 C   s"   |   sg S tdd |  D S )zALiefert sortierte Liste aller EDA-Report-Dateien (RC*.xlsx/.xls).c                 s   s2    | ]}|j  d v r|j dr|V  qdS )r   r   Nr   r   r   r   r8   r   r"   r"   r#   r:     s    z"_find_eda_files.<locals>.<genexpr>)r   r   r   )r   r"   r"   r#   _find_eda_files  s
   r
  c                 C   r  )uP   Findet eine hochgeladene Abrechnungsinfo.json (Dateiname enthält 'abrechnung').Nr  
abrechnungr   r   r"   r"   r#   _find_letzter_lauf_file  r  r  zF_(\d{4}-\d{2}-\d{2})T\d{2}[_:]\d{2}-(\d{4}-\d{2}-\d{2})T\d{2}[_:]\d{2}	eda_filesc                 C   s`   g g }}| D ]}t t|j}|r#||d ||d q|s(dS t|t|fS )u   Parst Abrechnungszeitraum aus EDA-Dateinamen.
    Gibt (periode_von, periode_bis) als 'YYYY-MM-DD'-Strings zurück, oder (None, None).r   ro   )NN)_EDA_DATE_REsearchr   r   r   groupminmax)r  startsendsr   mr"   r"   r#   _parse_eda_period  s   
r  tdrl   c                 C   s  t | } | d }|jdd | d jdd | d jdd | d d jdd |d}|d j|r5|ntd	d
 |dd}d}|dd}|dd}|dd}dtdtfdd}	i d|	|dddd|	|dddd|ddd|d |d!dd"|d#dd$|d%dd&|d'dd(|d)dd*|d+dd,|d-dd.|d/d0d1|d2dd3|d4d5d6|d7d8d9|d:dd;||||d<d=|d>d|d?d|d@d|dAd|dBd|dCddD	}
|dE jtj|
dFdGdHdId
 |r#t | d | }| r#t	
|||  |r;t | d | }| r;t	
|||  tdJD ]}t	
|||j  q@tt}|d<r]|d< |dK< |d>rm|d> dLdM|dN< |dO jtj|dFdGdHdId
 dPS )QzPLegt Config/ + uploads/ im Temp-Verzeichnis an und schreibt alle Konfig-Dateien.ConfigTr.   r   
RechnungenReport_config_csv_override
Config.csv	utf-8-sigrV   mitglieder_filenamezMitglieder.xlsmKassabuch.xlsxvorlage_filenamer2   logo_filenameaccent_colorr   rF   c              	   S   sJ   zt t| dddddddW S  ttfy$   | Y S w )z^Normalisiert eine Zahl: Komma als Dezimaltrenner.
        main.py erwartet Strings wie '0,12'.r   .z.4f0)floatrO   replacerstrip
ValueError	TypeError)valr   r"   r"   r#   _num  s
   .z#_setup_temp_structure.<locals>._numTarif_Einspeisungtarif_einspeisungz0,10Tarif_Bezugtarif_bezugz0,12   Fälligkeitfaelligkeit14File_MitgliederVereinsnamevereinsnameVereinsstrassevereinsstrasse
Vereinsort
vereinsort	SEPA_IBAN	sepa_ibanSEPA_BICsepa_bicSEPA_Creditor_IDsepa_creditor_idBankbankKleinunternehmerkleinunternehmerJa
UID_Nummer
uid_nummerRC_Kostentraegerrc_kostentraegerMitgliedAbbuchung_Modusabbuchung_modusueberweisungAV_Emailav_emailFile_Rechnungsnummermail_betreffzEEG Abrechnung {Periode}	mail_bodysmtp_server	smtp_mode	smtp_user	smtp_passmail_reply_to)	Logo_FilenameAccent_ColorMAIL_Betreff	MAIL_BodySMTP_Server	SMTP_Mode	SMTP_User	SMTP_PassMail_ReplyToParameter.jsonFro   rp   r=   z
SEPA_*.xmlry   z\n
r}   zemail_vorlage.jsonN)r   mkdirrN   rs   STANDARD_CONFIG_CSVrO   rg   rt   rf   shutilcopySEPA_TPL_DIRglobr   dictDEFAULT_EMAIL_VORLAGEr&  )r  rl   
config_dircustom_configmitglieder_filekassabuch_namevorlage_namer!  r"  r+  
param_datavorlage_srclogo_srcxml_fileemail_vorlager"   r"   r#   _setup_temp_structure  s   

	










rt  contentc              	   C   s   i }i ddddddddd	d
dddddddddddddddddddddd d!d"d#d$d%d&d'd(d)d*}z#t | }| D ]\}}||v ra|d+u rYd,nt|||| < qKW |S  tyn   Y |S w )-uM   Liest Parameter.json-Inhalt und gibt ein Dict mit den Formularwerten zurück.r,  r-  r.  r/  r0  r1  r4  r5  r6  r7  r8  r9  r:  r;  r<  r=  r>  r?  r@  rA  rB  rC  rE  rF  rG  rH  rW  r!  rX  r"  rJ  rK  rM  rN  rP  rQ  rR  rS  rT  rU  rV  )rY  rZ  r[  r\  r]  r^  r_  Nr2   )rg   rh   r   rO   rQ   )ru  resultmappingrn   r   r   r"   r"   r#   _parse_parameter_jsonZ  sp   	

rx  )pdfsreport	kassabuchseparechnungsversandabrechnungsvereinc                 C   s  |du rt tndd |D }t| } t }| d }t|dtj}d|v rM|| }| rMt	|
dD ]}| rL||d| d	||  q7d
|v r_|d }	|	 r_||	d d|v sgd|v r|d }
|
 rvt	|
dng D ]}||d|j  qxd|v r|d }| rt	|dD ]}||d|j  q| d }| r||d W d   | S W d   | S 1 sw   Y  | S )u  Packt nur die Output-Dateien der ausgewählten Module in ein ZIP.

    Args:
        td:       Session-Tempordner.
        periode:  Abrechnungsperiode (Ordnername unterhalb von Rechnungen/).
        modules:  Iterable mit Modul-Keys ("pdfs", "report", "kassabuch",
                  "sepa"). ``None`` bedeutet "alle Module" (Rückwärts-
                  kompatibilität zu Clients ohne Modul-Auswahl).
    Nc                 S   s   h | ]}|r|qS r"   r"   r8   r  r"   r"   r#   	<setcomp>      z_build_zip.<locals>.<setcomp>r  wry  *zRechnungen/r   r{  r  zRechnungen/Kassabuch.xlsxr|  r~  SEPAz*.xmlzSEPA/rz  r  z*.pdfzGemeinschaftsbericht/Abrechnungsinfo.json)setALL_MODULESr   ioBytesIOzipfileZipFileZIP_DEFLATEDrf   r   rglobis_filewriterelative_torg  r   getvalue)r  r   modulesmodsbufrechnungen_basezfperiode_dirr   r{  sepa_dir
report_dirainfor"   r"   r#   
_build_zip  sF   

$
$$r  c                       s0   e Zd ZdZdejf fddZdd Z  ZS )_QueueHandleru   Schreibt Log-Records in eine Queue. Bei voller Queue wird die Nachricht
    verworfen – so kann der main.py-Logger nie blockieren, auch wenn der SSE-
    Client nicht mitliest.qc                    s   t    || _d S r~   )super__init__r  )selfr  	__class__r"   r#   r    s   

z_QueueHandler.__init__c                 C   sd   z| j j|jt d| |ddd W d S  tjy#   Y d S  t	y1   | 
| Y d S w )N%H:%M:%Sr)   r   rE   g      ?timeout)r  put	levelnamer   r   strftimer*   queueFullrQ   handleError)r  recordr"   r"   r#   emit  s   z_QueueHandler.emit)	__name__
__module____qualname____doc__r  Queuer  r  __classcell__r"   r"   r  r#   r    s    r  c                 C   s   t | trt| st| rdS | S t | tr| S t | tr!| S | du r'dS t | tr.| S t | tr<dd | 	 D S t | t
ttfrKdd | D S zt| W S  tyZ   Y dS w )zErsetzt NaN/Infinity rekursiv durch 0.0 bzw. None und wandelt Sentinels
    in JSON-sichere Werte um. Wird vor json.dumps() aufgerufen, damit der
    Browser nie ein literales `NaN` im Stream sieht (ungueltiges JSON).g        Nc                 S   s   i | ]\}}t |t|qS r"   )rO   _sanitize_json_value)r8   r   r*  r"   r"   r#   
<dictcomp>  s    z(_sanitize_json_value.<locals>.<dictcomp>c                 S      g | ]}t |qS r"   r  )r8   xr"   r"   r#   r     r  z(_sanitize_json_value.<locals>.<listcomp>r2   )r3   r%  _mathisnanisinfr   intrO   rh  r   listtupler  rQ   )r   r"   r"   r#   r    s*   





r  c              
   C   sh   zt jt| dddW S  ttfy3 } zt jddt|j d| ddddW  Y d}~S d}~ww )u   json.dumps mit allow_nan=False + vorab-Sanitisierung.
    Wirft nie – im Fehlerfall kommt ein minimaler Fehler-Frame raus, der sich
    vom Browser weiterhin parsen laesst.F)rq   	allow_nanzSerialisierungs-Fehler: : okr&   N)rg   rt   r  r)  r(  typer  )payloader"   r"   r#   _safe_sse_json  s   
r  r   z/EEG-Assistent/abrechnungc                   C      t dS )Nz
index.htmlr   r"   r"   r"   r#   index     r    r   i  i  i  )invalidr   lockeddisabledexpired	no_tokensz/api/v1/loginPOST)methodsc            
      C   s  t jpi } | dpd }| dpd }t }t jdd}td| ddds7td	|  t	dS z
t
||||}W nn t
jy } zBt|jd
}td|j d|  t||jd}|jdkrzt
t
||d< W n	 ty   Y nw t||fW  Y d}~S d}~w ty } ztd tddddfW  Y d}~S d}~ww td|d}	t|	|d  |	S )u   Login mit UID + Browser-Fingerprint.
    Body: {"uid": "EEGA-XXXX-XXXX-XXXX", "fingerprint": "<hex>"}
    Erfolg: Cookie wird gesetzt + Kontostand zurückgegeben.
    uidr2   fingerprint
User-Agentz	login_ip:   r   r   r   u    Login-Burst-Limit aktiv für IP r  zLogin abgelehnt () von r   r  lock_expires_atNzLogin-FehlerzInterner Fehler beim Login.server_error  T)r  accountr   )r   rg   rN   r   r   r   r   rM   r   r   r   loginLicenseError_LICENSE_HTTPr   rO   get_lock_expires_normalize_uidrQ   r	   	exceptionr   )
r}   r  r  r   uar   r  status	resp_datar   r"   r"   r#   	api_login  sH   


r  z/api/v1/logoutc                  C   sT   t  } | rzt|  W n ty   td Y nw tddi}|jtdd |S )u+   Session-Lock freigeben und Cookie löschen.zLogout-Fehlerr  Tr   r   )	r   r   logoutrQ   rM   r  r	   r   r   )r   r   r"   r"   r#   
api_logoutD  s   r  z/api/v1/sessionGETc                  C   sb   t  } | r
t| nd}|du r!tddi}| r|jtdd |S td|d}t||d  |S )	u   Liefert den aktuellen Lizenz-Status (für Seiten-Reload / Header-Refresh).
    Antwort: {"logged_in": bool, "account": {…}} — kein Fehler wenn nicht eingeloggt.
    N	logged_inFr   r   T)r  r  r   )r   r   r   r	   r   r   r   )r   r   r   r"   r"   r#   api_session_statusR  s   r  z/api/v1/packagesc                  C   J   zt jdd} W n ty   td tddidf Y S w td| iS )uF   Öffentlicher Paket-Katalog (Admin bearbeitbar). Für Schritt 3 im UI.Tonly_activez)Paket-Katalog konnte nicht geladen werdenr&      Paket-Katalog nicht verfügbar.r  packagesr   list_packagesrQ   rM   r  r	   pkgsr"   r"   r#   api_packagesd     
r  z/api/v1/session/newc                  C   s   t  } td| iS )Nr   )smcreater	   r   r"   r"   r#   api_new_sessionq  s   r  z/api/v1/uploadc                  C   s  t jd} t| rt| stddidfS tt| }|d }|jddd g }g }t j	
 D ]\}}|r;|js<q2t|j}|rG|dv rN||j q2|| }z| |  W n tyl   ||j Y q2w z	|t| W n* ty }	 ztd	| d d
  d| d|	  ||j W Y d }	~	q2d }	~	ww || td	| d d
  d|  q2tj| dd d|i}
|r||
d< t|
S )Nr   r&      Ungültige Sessionr  r   Tparentsr/   >   ..r#  [   z] Upload fehlgeschlagen (): z
] Upload: 	uploadingr  savedrejected)r   formrN   r   r  r	   r   temp_dirrb  filesr   filenamer   r   resolver  r(  saverO   OSErrorrM   warningr   update)r   r  r   r  r  fieldfobjfnamedestr  r   r"   r"   r#   
api_uploadx  sJ   

$
 r  z/api/v1/remove-filec                  C   s  t jpi } | d}| dd}t|rt|s!tddidfS tt|d }t|}|r}|| }z"|	 
|	  | rU|  td|d	d
  d|  W n& ttfy| } ztd|d	d
  d| d|  W Y d	}~nd	}~ww tddiS )u>   Löscht eine einzelne Datei aus dem Upload-Ordner der Session.r   r	  r2   r&   r  r  r   r  Nr   z] Datei entfernt: z&] Datei konnte nicht entfernt werden (r  r  T)r   rg   rN   r   r  r	   r   r  r   r
  r  rf   unlinkrM   r   r(  r  r  )r}   r   r	  r   safetargetr  r"   r"   r#   api_remove_file  s(   

0r  
MitgliederIDVornameNachnameSexAdressePLZOrtMailIBANBICCONSUMPTION
GENERATION)r  r  r  r  r  r  r  r   r!  r"  r#  r$  c                 C   s,   | du rdS t |  }|dkp| dkS )z3True wenn Zellwert als leer gilt (None, leer, NaN).NTr2   nan)rO   r   r   )r   r   r"   r"   r#   	_is_empty  s   r&  z!/api/v1/validate-uploaded-membersc            "         s<  t jpi } | d}t|rt|stddidfS tt|d }| s/tdddS t	| d	d
}|rD|| 
 rD|| }nt|}|du rStdddS ddl}td }|
 r|||  }||  }||kr|tdddS zddlm}	 |	t|ddd}
W n ty } ztdd| dW  Y d}~S d}~ww t|
jvr|
  tddt dd|
j ddS |
t }|jdd}zt|}W n ty   |
  tddd Y S w i  t|D ]\}}|du rq| t| < q fddt D }|r%|
  tddt dd| ddS dd d!} fd"d|  D }dd#l!m"} g }d}t|d$d%D ]\}du rPqE fd&d'}|d(}|d)}t#|rkt#|rkqE|d*7 }|d+}|du st$|t%r||krd
}nztt&|}W n t't(fy   t| }Y nw d, v r|d,nd}||d-|d.|d/|d0|d1|d2||d3|d4|d5|d6}|r|d.pd
}|d/pd
} | d7|   pd8}!|)|t|d-pd
 |!|d9 qE|
  |dkrtdd:dS tt*|dk||j+||d;S )<ug  Prüft die hochgeladene Mitgliederdatei direkt nach dem Upload.
    Setzt die Standard-Spaltennamen voraus (wie in Mitglieder_Vorlage.xlsx).

    Response:
      { "ok": true,  "count": 42, "errors": [] }
      { "ok": false, "count": 42, "errors": [{zeile, id, name, fehler}, …] }
      { "ok": false, "error": "…" }  – bei strukturellen Problemen
    r   r&   r  r  r   Fz$Kein uploads/-Ordner in der Session.r  r	  r2   Nz)Keine Mitgliederdatei im Upload gefunden.r   Mitglieder_Vorlage.xlsxuh   Es wurde die leere Vorlage hochgeladen. Bitte die echte Mitgliederliste hochladen – nicht die Vorlage.)load_workbookT)	data_only	read_onlyu%   Datei konnte nicht geöffnet werden: zTabellenblatt 'z' nicht gefunden (vorhanden: , ).values_onlyu   Datei enthält keine Daten.c                       g | ]}| vr|qS r"   r"   )r8   labelcol_idxr"   r#   r   "  r   z1api_validate_uploaded_members.<locals>.<listcomp>zFehlende Spalte(n) in 'z': uA   . Bitte Überschriften aus der Mitglieder_Vorlage.xlsx verwenden.zSUnternehmen mit Reverse Charge (Werte: 0 = Privat, 20 = Unternehmer, 13 = Landwirt)u2   Firmenname für die Rechnung (statt Vor-/Nachname))
SteuersatzRechnungsnamec                    s(   g | ]\}}| vrd | d| qS )zSpalte "z
" fehlt - r"   )r8   coldescr1  r"   r#   r   0     ( )validate_member_rowro   )startc                    s     |  }|t k r| S d S r~   )r   )r0  idxr2  rowr"   r#   cell9  s   z+api_validate_uploaded_members.<locals>.cellr#  r$  r   r  r3  r  r  r  r   r!  r"  r  r  r  )	member_idvornamenachnamemailibanbicplzortstrassesex
steuersatz z(ohne Namen))zeileidr   fehleru   Keine Mitglieder mit Verbrauchsdaten (CONSUMPTION oder GENERATION) gefunden. Bitte die echte Mitgliederliste hochladen – nicht die leere Vorlage.)r  countdateierrorshints),r   rg   rN   r   r  r	   r   r  r   r   rf   r   hashlibrf  md5
read_bytes	hexdigestopenpyxlr(  rO   rQ   _STANDARD_MITGLIEDER_SHEET
sheetnamesclosejoin	iter_rowsnextStopIteration	enumerater   _STANDARD_COLSvaluesr   mainr8  r&  r3   r%  r  r(  r)  r   r   r   )"r}   r   r   requested_filenamerl  rQ  _vorlage_path_vorlage_hash_upload_hashr(  wbr  ws	rows_iterr   ir9   missing_cols_OPTIONAL_COLSrP  r8  rO  checkedrow_numr=  consumption
generationplz_rawplz_str
steuer_rawerrr?  r@  r   r"   r;  r#   api_validate_uploaded_members  s   


 


 

rs  z/api/v1/validate-edac            &         s  t jpi } | d}t|rt|stddidfS tt|d }dh  fdd|d	D }|s>td
dgg dS g d}g }g }t	 }|D ](}|j
}	zddl}
|
jt|ddd}W n ty~ } z||	 d| d W Y d}~qKd}~ww t|jdk r||	 d |  qKt|jd jdd}|  t|dk r||	 dt| d qKg d}d}|D ]8\}}|| r|| d rt|| d  nd}||s||	 d|d  d| d|dd   d! d
}q|sqK|d }h d"}t	 }|dd D ]"}|durt| nd}|r,||vr,|| || q|s:||	 d# qK|d$ }d%d& |D fd'd|D }|r_||	 d(d)|  qKtd*|dd+  d,|	 d-t| d. qK|rtd
||dS z fd/d|d0D }|r6|r6ddl}|jt|d ddd}d1|jv r6|d1 }d2d t|jdd3D }d4|v r|d4nd}d5|v r|d5nd} t	 }!|jddd6D ]-}"|dur|"| r|!t|"|   | dur|"|  r|!t|"|    q|  ||! }#|#r"|d7d)t|#  |!| }$|$r6|d8d)t|$  W n tyS } z|d9| d W Y d}~nd}~ww t|dk}%t|%||t|d:S );us   Prüft EDA-Datei(en) auf korrektes Format und gleicht Zählpunkte
    mit der Mitgliederliste ab (falls vorhanden).r   r&   r  r  r   r   c                    s4   g | ] t  fd dD s j dkr qS )c                 3       | ]
}| j  v V  qd S r~   r   r   r7   r   r"   r#   r:         .api_validate_eda.<locals>.<listcomp>.<genexpr>r  )anyr   r   r8   MEMBER_HINTSrv  r#   r     s    z$api_validate_eda.<locals>.<listcomp>z*.xlsxFzKeine EDA-Datei gefunden)r  rO  warnings)z.Eigendeckung gemeinschaftliche Erzeugung [KWH]zFErzeugung lt. Messung entsprechend dem Teilnahmefaktor und EC-ID [KWH]u&   Restüberschuss bei EG und je ZP [KWH]z)Gesamte gemeinschaftliche Erzeugung [KWH]z@Gesamtverbrauch lt. Messung (bei Teilnahme gem. Erzeugung) [KWH]r   NT)r*  r)  u'   : Datei konnte nicht geöffnet werden ()ro   uA   : Weniger als 2 Tabellenblätter — Blatt 2 (Energiedaten) fehltr   r-     z: Blatt 2 hat nur u-    Zeilen — EDA-Format erwartet mindestens 16))r   MeteringPointId)   Energydirection)   	MeterCode)   Spaltensummer2   z: Zeile z soll mit 'z' beginnen, gefunden: '(   '>   r2   MMNoneTOTALr  u'   : Keine Zählpunkte in Zeile 2 gefundenr  c                 S   s(   h | ]}|rt | rt | qS r"   )rO   r   )r8   r   r"   r"   r#   r    r7  z#api_validate_eda.<locals>.<setcomp>c                    r/  r"   r"   )r8   mc)present_codesr"   r#   r     r   z: Fehlende MeterCodes: r+  r  r   z] EDA-Check OK:     — u    Zählpunktec                    s&   g | ] t  fd dD r qS )c                 3   rt  r~   ru  r7   rv  r"   r#   r:     rw  rx  )ry  rz  r{  rv  r#   r     s    z*.xls*r  c                 S   s$   g | ]}|j rt|j  nd qS )r2   )valuerO   r   )r8   cr"   r"   r#   r     r   )max_rowr#  r$  )min_rowr.  uF   EDA enthält Zählpunkte ohne Mitglied (Abrechnung würde abbrechen): z<Mitglieder ohne EDA-Eintrag (werden mit 0 kWh abgerechnet): u#   Mitglieder-Abgleich übersprungen ()r  rO  r}  eda_zp_count)r   rg   rN   r   r  r	   r   r  rg  r  r   rU  r(  rO   rQ   r   r   rW  rX  r  
worksheetsrZ  r   r   addrY  rM   r   r[  r  r   )&r}   r   r   r  REQUIRED_METER_CODESrO  r}  eda_zpseda_pathr  rU  re  r  rows
key_checksrow_okr:  expected_prefixr=  row_zpSKIPfile_zpsr   r   row_codemissing_codesmitglieder_files_opxwb_mws_mheader_mcons_idxgen_idx
member_zpsr<  eda_ohne_mitgliedmitglied_ohne_edar  r"   )r|  r  r#   api_validate_eda  s   



,
&

,
r  z/api/v1/parse-configc               
   C   sB  t jpi dpt jd} t| rt| stddidfS tt| }|d }t	|}|s7tdi dS z	|j
dd	d
}W n ty] } ztdi d| dW  Y d }~S d }~ww t|}t|}|rz	|j
dd	d
}W n ty|   d }Y nw |rt| }	t|	pi dpi }
||
d< tj| |
d td|dS )Nr   r&   r  r  r   F)foundr_  r  r&  )r@   rO  z,Parameter.json konnte nicht gelesen werden: )r  r_  r&   rn   r  ru   T)r   rg   rN   r  r   r  r	   r   r  r  ri   r  rx  r  rh  r  )r   r  r   
param_fileru  r  r_  config_fileoverrider   new_datar"   r"   r#   api_parse_config  s<   
r  z/api/v1/configc                  C   s  t jpi } | d}t|rt|stddidfS tt|}|d }t|}|s4tddidfS t	|}t
| }|j|d< |rH|j|d< t| d	d
}t|pVi }|dp]i d}	|	rg|	|d< z	tt|| W n* ty }
 ztd|d d  d tdd|
 idfW  Y d }
~
S d }
~
ww ||j }| r| st|| dd t|D }t|\}}d }t|}|rz%t|jdd}td|d d  d|d	d d|dd  W nZ ty } ztd|d d  d|  W Y d }~n=d }~ww | drF| d }t|t
rF|ds*|drF|}td|d d  d|dd d|dd  tj|di t|pRi dpXi ||| d d
|||d!d" td#d$d |D |d%S )&Nr   r&   r  r  r   zKeine Mitglieder-Datei gefundenr  r   r   r2   rn   r  r  r   z] Setup fehlgeschlagenz/Konfiguration konnte nicht geschrieben werden: r  c                 S   r  r"   )rO   r	  r"   r"   r#   r   i  r  z#api_save_config.<locals>.<listcomp>r=   rV   z0] Abrechnungsinfo.json eingelesen: letzter Lauf ?z, letzte Nr letzte_rechnungsnrz4] Abrechnungsinfo.json konnte nicht gelesen werden: letzter_lauf_manualperiode_bisz] Letzter Lauf manuell: Nr=z, bis=
configuredrechnungsdatum)r  r   r  periode_vonr  letzter_lauf)r  rn   Tc                 S   s   g | ]}t |jqS r"   )r   r   r	  r"   r"   r#   r     s    )r  r  r   )r   rg   rN   r   r  r	   r   r  r   r   rh  r   r   rt  rO   r  rM   r  rf   rd  re  r
  r  r  rh   ri   r   rQ   r  r3   r  )r}   r   r  r   mitglieder_pathvorlage_pathrl   periode_safecurrentr  r  dstr  r  r  r  letzter_lauf_filee_llll_mr"   r"   r#   api_save_config=  s   



"


*$




r  z/api/v1/preview-streamc               	      s  t jdtstddidfS t} | s!tddidfS | dp'i   dp.g  d	d
ttjddt	
 i fdd}t	j|ddd  d"dtdtffdd f	dd}t	j|ddd  fdd}t| ddddd S )#u   
    Führt system.prepare() in einem Worker-Thread aus und streamt den Fortschritt
    als Server-Sent Events. Abschluss-Nachricht: event: done, data: <result-json>
    r   r&      Ungültige Session-IDr  Session nicht gefunden  rn   r  r   r2     maxsizec                      sP      s&zjddidd W n
 tjy   Y nw  jdd    rd S d S )Nr  	keepalive      ?r         @)is_setr  r  r  waitr"   )prep_done_evtr  r"   r#   	heartbeat  s   z%api_preview_stream.<locals>.heartbeatTz
preview-hbr  daemonr   rD   msgr)   c                    H   t | | z j|t d| ddd W d S  tjy#   Y d S w Nr  r  r  r  rS   r  r   r   r  r  r  )r  r)   r  r"   r#   r    s   
z api_preview_stream.<locals>.emitc            &   
      sL  dd l  dVdtf fdd} t}|td t }|| zzt	}|d d }|d d }d	 |	 sd| d
|j
 d W W || dd l}|d ddi d S |	 s| d|j
 d W W || dd l}|d ddi d S dd | D }|sdd | D }|s| d W W || dd l}|d ddi d S d|d j
  D ],}t|	 s| dt|j
   W W || dd l}|d ddi d S qɈdt d d ddlm}	 |		}
|
jdd}|rd|  dd}|rWz$t|d}||
_|tt|
jdd d! |
_d"|d#  W n
 tyV   Y nw d$ t}d%| d&|d'krkd(nd d) D ]}d*t|j
  qsd+ |
}d,d |pg D }  d-d |D }td.d/ |D }td0d/ |D }td1d/ |D }d2t| d3t| d4 |s|rڈd5| d6|  d7}d8}d9}|
jj }|r|d9}|d:|pd;}|d<d;}|r|r||krd=| d>| d?d@dA nM|rj|rjz<ddBlm!}m} |"|}|"|}||d'd! } || kr^| d#}!||d'd! d#}"dC|! dD|" dEd@dA W n
 tyi   Y nw |dFd}#t|dGdpyd}$|##t|r|$dkrdH| dI|$d' dJdK|# d? ndL| dM n|rdN| dO t$j%|
dP %dQ|t|t&|dRdSdT W n t'y }% z| dU|% W Y d }%~%nd }%~%ww W || dd l}|d ddi d S W || dd l}|d ddi d S || dd l}|d ddi w )WNr   r  c                    s      t|t}|s d|  d |durF|sFt|j}t|p%t|} d| d| d   D ]}|	 rE d| d q7|  |r]|s]dt|j dt|pZt| nd }
d|d	 dS )
u*  Einheitliche Fehlerbehandlung: Log + result_holder setzen.

            Bei SystemExit wird KEIN Traceback angezeigt – die eigentliche
            Fehlermeldung wurde vorher bereits via logger.error() in die
            SSE-Queue geschrieben (durch den _QueueHandler am Root-Logger).
               ❌ rI   N   r  r  r2   Fr  )r  r3   
SystemExitr  r  rO   repr
format_exc
splitlinesr   r  )r  excis_systemexitexc_typeexc_strlinefull_err)r  r  result_holdertb_modr"   r#   fail  s(   

$z0api_preview_stream.<locals>.worker.<locals>.fail%(message)sr  r  r`  u$   🔍 Konfiguration wird geprüft …zConfig/Config.csv fehlt in u"    – Konfiguration unvollständig.g?rE   __PREVIEW_DONE__zConfig/Parameter.json fehlt in c                 S   s,   g | ]}|j  d v rd|j v r|qS )r   r   r   )r   r   r   r	  r"   r"   r#   r     s    z6api_preview_stream.<locals>.worker.<locals>.<listcomp>c                 S   s.   g | ]}|j  d v r|j ds|qS )r  r   r  r	  r"   r"   r#   r     s    zDKeine Mitglieder-Datei (.xlsm/.xlsx) im Arbeitsverzeichnis gefunden.u      ✓ Mitglieder: zEDA-Datei nicht gefunden: u      ✓ EDA-Dateien: z
 vorhandenu-   🔧 Abrechnungssystem wird initialisiert …)Abrechnungssystemr4  r2   u   🏢 Verein: r  %Y-%m-%dr0     )daysu   📅 Rechnungsdatum: z%d.%m.%Yu%   📋 Mitgliederliste wird gelesen …u   ⚡ z
 EDA-Dateir   enz werden eingelesen:u      • u:   🧮 Kosten werden berechnet – das kann etwas dauern …c                 S   r  r"   r  r  r"   r"   r#   r   (  r  c                 S   s   g | ]	}| d r|qS )has_invoicerN   r  r"   r"   r#   r   +  s    c                 s   s    | ]}|d  V  qdS )kostenNr"   r  r"   r"   r#   r:   ,  s    z5api_preview_stream.<locals>.worker.<locals>.<genexpr>c                 s   s$    | ]}| d ddkrdV  qdS r  r   r   Nr  r  r"   r"   r#   r:   -     " c                 s   s$    | ]}| d ddk rdV  qdS r  r  r  r"   r"   r#   r:   .  r  u   ✅ z Mitglieder geladen, z mit Rechnung.u      ↳ Lastschriften: z  |  Gutschriften: r  r  r  r   r  abrechnungsdatumu"   ⚠ Zeitraum bereits abgerechnet: z (letzter Lauf: r~  rH   r)   )dater   u    ⚠ Abrechnungslücke: Zeitraum u   –z wurde nicht abgerechnet.r  letzte_laufnru)   🔢 Rechnungsnummern: Weiterführung ab -04dz (letzter Lauf endete bei u9   🔢 Rechnungsnummern: Neuer Jahreskreis – beginnt bei z-0001u$   🔢 Rechnungsnummern: beginnen bei z%-0001 (kein letzter Lauf hochgeladen))systemTro   )rM  r  )r  memberstotalsu)   Unerwarteter Fehler während der Vorschaur~   )(	tracebackrO   r  setFormatterrK   	Formatter	getLogger
addHandlerr   rf   r   removeHandlerr   sleepr  r   r   r`  r  r  rN   r   strptimedatum_rechnungr   r  datum_abbuchungr  rQ   preparer  sumyearr  fromisoformatr   r  r  roundBaseException)&r  _log_handler_root_loggertd_path
config_csv	param_csvr   
xlsm_filesefr  r  r5  rd_strrdn_edar   r  invoicedtotal_kls_countgs_countr  curr_voncurr_biscurrent_yearprev_bis
prev_label
prev_datum_date_td
prev_bis_d
curr_von_dexpectedgap_vongap_bisletzte_nr_strr  r  	rn   r  r  r   r  r  r  r   r  )r  r#   worker  s,  

|
 
y

m

f

$















z"api_preview_stream.<locals>.workerzpreview-workerc                  3   sh    	 z j dd} W n tjy   dV  Y qw |  ddkr*dt dV  d S d	t|  dV  q)
NTr<   r  zdata: {"type":"keepalive"}

rE   r  zevent: done
data: 

data: rN   r  Emptyr  item)r  r  r"   r#   streamv  s   z"api_preview_stream.<locals>.streamtext/event-streamno-cachenozCache-ControlzX-Accel-Bufferingmimetyper   NrD   )r   r   rN   r   r	   r  r  r  r  	threadingEventThreadr9  rO   r   )r   r  r*  r1  r"   r)  r#   api_preview_stream  s2   


 /r<  z/api/v1/processc                     s  t jpi dd} | r!td|  ddds!td|   tdS t jd	ts3t	d
didfS t
}|sBt	d
didfS |dd u rSt	d
didfS t jdd }|rdd |dD }zdd tjddD W n ty   td t	d
didf Y S w fdd|D }|rt	d
dd| idfS tt|st	d
didfS nttt
jd  |d!pi  t d"dt
tjd#d$d5d&td'tffd(d) fd*d+}tj|dd,d-  fd.d/}t| d0d1d2d3d4S )6N
uid_maskedr2   zprocess_uid:r  r   r  u   Process-Burst-Limit aktiv für r<   r   r&   r  r  r  r  r  zErst /api/v1/preview aufrufenr  c                 S   s   g | ]
}|  r|  qS r"   )r   r  r"   r"   r#   r     s    zapi_process.<locals>.<listcomp>r   c                 S   s   h | ]}|d  qS )r   r"   )r8   pr"   r"   r#   r    r  zapi_process.<locals>.<setcomp>Tr  z)Paket-Katalog nicht ladbar in api_processr  r  c                    r/  r"   r"   r  )
valid_keysr"   r#   r     r   zUnbekannte(s) Modul(e): r+  u    Mindestens ein Modul auswählen.)r  rn   r   r  r  rD   rE   r)   c                    r  r  r  )rE   r)   r  r"   r#   r    s   
zapi_process.<locals>.emitc                     s  dd l } t}|td t }|| d}zzt}ddt	|  d|v rfd} 
d}|r[|
dd	}t|
d
dpJd}|dkr[|tjjr[|}d j|d nd d|v rz*ddlm}	 td d }
|
jddd t|
d d }|	| d W n% ty } zdt|j d| dd W Y d }~n	d }~ww d d|v sd|v rψd   nd d |v rd!   nd" d#|v rd|vrd$dd ntd%d& jD }d'| d( nd) d|v r/j
d*d+  }|dkr%d, n
d-| d.dd tj d/d0 d}W nQ t!y } zDt"|t#}|std1$d2 d3t|j dt|pbt%| d4d | & ' D ]}| r~d5| d4d qnW Y d }~nd }~ww W |(| zj)d6|d7d8d9 W d S  t*j+y   Y d S w |(| zj)d6|d7d8d9 W w  t*j+y   Y w w ):Nr   r  Fu   🧩 Ausgewählte Module: r+  ry  r  r  r2   r  u'   📄 Rechnungs-PDFs werden erstellt …)start_rechnungsnummeru+   ⏭ PDFs übersprungen (nicht ausgewählt).rz  )generate_community_reportr  r  Tr  Gemeinschaftsbericht_z.pdfu#   📊 Gemeinschaftsbericht erstellt.u7   ⚠ Gemeinschaftsbericht konnte nicht erstellt werden: r  rH   r  u;   ⏭ Gemeinschaftsbericht übersprungen (nicht ausgewählt).r|  r~  u   💳 SEPA-XML wird erstellt …u/   ⏭ SEPA-XML übersprungen (nicht ausgewählt).r{  u#   📒 Kassabuch wird geschrieben …u0   ⏭ Kassabuch übersprungen (nicht ausgewählt).r}  um   ⚠ Rechnungsversand übersprungen – PDFs wurden nicht erstellt (Modul 'PDF-Rechnungen' nicht ausgewählt).c                 s       | ]
}|j d krdV  qdS r   r   NKostenr  r"   r"   r#   r:     rw  z+api_process.<locals>.run.<locals>.<genexpr>u    📧 Rechnungsversand aktiv – z< Mail(s) werden nach dem ZIP-Download automatisch versendet.u7   ⏭ Rechnungsversand übersprungen (nicht ausgewählt).rJ  rL  ui   🏦 Abrechnungsverein-Modus aktiv – Abbuchung durch 'Verein zur Abrechnung von Energiegemeinschaften'.uQ   ⚠ Modul 'Abrechnungsverein' gewählt, aber Parameter.json hat Abbuchung_Modus='u   '. Bitte Parameter prüfen.doner  process-workerz$Abrechnungs-Verarbeitung abgebrochenr  rI   r  __DONE__)rE   successr  r  ),r  r  r  rK   r  r   r  r  rY  r   rN   r  r   rO   r  r	  generate_pdfsrz  rA  r   rb  rQ   r  r  build_sepa_xmlwrite_kassabuchr  
mitgliederr  r   r   r  r  r  r3   r  r  r  r  r  r  r  r  r  )r  handlerroot_loggerrJ  r  	_start_nr_ll_letzte_nr_str_letzte_laufnrrA  	rp_folderrp_pathe_repn_mailsmodusr  r  r  )rn   r  r  r   r  r   r  r  r"   r#   run  s   










,


zapi_process.<locals>.runrH  r  c                  3   sV    	 z j dd} W n tjy   dV  Y qw dt|  dV  |  ddkr*d S q)	NT   r  zdata: {"type": "keepalive"}

r,  r+  rE   rI  r-  r/  r  r"   r#   r1  :  s   zapi_process.<locals>.streamr2  r3  r4  r5  r6  r8  ) r   r   rN   r   rM   r   r   r   r   r	   r  r   r   r   r  rQ   r  rY  r   r  r  r  r  r   r  r  r  rO   r9  r;  r9  r   )lic_keyr   modules_raw	requestedunknownrZ  r1  r"   )	rn   r  r  r   r  r   r  r  r?  r#   api_process  s`   



	kr`  z/api/v1/downloadc                     s  t jpi dd} | r!td|  ddds!td|   tdS t jd	ts3t	d
didfS t
}|sBt	d
didfS |ddkrQt	d
didfS t
}t|dp]i dd|dpii }|dprtt}|d}|d urzQt|dg \}}t|ddpd}|r|jj d|dnd}	d|jdd||t dtdd  |jD |	|d!	}
t|d" jtj|
d#d$d%d&d' W n! ty } ztd(d d)  d*|  W Y d }~nd }~ww zt||}W n* ty! } zt d(d d)  d+ t	d
t!|id,fW  Y d }~S d }~ww t" p'd}zt#j$||t% dt j&d-dd.}W nY t#j'yu } z,t(|j)d}td(d d)  d/|j) d0|  t	t!||j)d1|fW  Y d }~S d }~w ty   t d(d d)  d2 t	d3d4d1d,f Y S w td(d d)  d5|d6  d7|d8  d9|d:d; d<	 t*| |d fd=d>}t+j,|d?d@-  dA dBt dC dD}t.t/0|dEd?|dF}|j1t2dGdH |S )INr=  r2   zdownload_uid:
   r   r  u    Download-Burst-Limit aktiv für r<   r   r&   zUngueltige Session-IDr  z3Session nicht gefunden oder bereits heruntergeladenr  r  rG  z%Verarbeitung noch nicht abgeschlossenrn   r   r   r  r  r  rechnungsnummerr   r  r  1r4  r  c                 s   rC  rD  rE  r  r"   r"   r#   r:   z  s    zapi_download.<locals>.<genexpr>)	versionr5  r   r  r  r  anzahl_rechnungenr  r  r  Fro   rp   r=   rV   r  r   z8] Abrechnungsinfo.json konnte nicht geschrieben werden: z] ZIP-Fehlerr  r  )r   r  r   r  
user_agentr   z] Token-Abzug abgelehnt: z -- r   z] Token-Abzug fehlgeschlagenzToken-Abzug fehlgeschlagen.r  z] Download: tokens_consumedz Token(s) abgezogen, remaining_tokensz uebrig (Kunde: customer_namer  r,  c                     s  d v r~d v r~dur~d} d}j D ]M}|jdkr`z| | d7 } W q ty_ } z.t|ddp8t|dd}td	dd
  d| dt|j d|  |d7 }W Y d}~qd}~ww q|rid| dnd}t	d	dd
  d|  d| d d}d v rdurz
| W n* ty } ztd	dd
  d| dt|j d|  W Y d}~nd}~ww t dS )zBMailversand (falls aktiv) + Session-Cleanup nach dem ZIP-Download.r}  ry  Nr   r   r   Namer  r  r   z
] Mail an  fehlgeschlagen: r  r+  z Fehlerr2   z] Mailversand abgeschlossen: z	 gesendetr#  zjakob@energie-durchdacht.atr~  z] SEPA-Versand an )rN  rF  
send_emailrQ   r5   rM   r  r  r  r   send_sepa_emailr  destroy)sentrO  r  e_m_namer   	_AV_EMAILe_avmods_setr   r   sys_objr"   r#   _finalize_after_download  sB   


	&z.api_download.<locals>._finalize_after_downloadT)r  r  EEG_Abrechnung_r   z%Y%m%dz.zipapplication/zipr7  as_attachmentdownload_namer   r   )3r   r   rN   r   rM   r   r   r   r   r	   r  r  r   r  r  r  r5   r  r	  r  r   r   r  r  rN  r   rs   rg   rt   rQ   r  r  r  rO   r   r   consume_tokensr   r   r  r  r   r  r9  r;  r9  r   r  r  r   r   )r\  r   r  rn   r  sys_obj_pre_pv_pbrT  
_letzte_rn
_lauf_infoe_ai	zip_bytesr  license_sidconsume_infor  rw  r	  r   r"   rt  r#   api_downloadN  s   



* 
&$


r  r   z/admin/c                   C   r  )zLLiefert das Admin-Panel. Auth passiert client-seitig via /admin/api/session.z
admin.htmlr  r"   r"   r"   r#   admin_index  s   r  z/admin/api/sessionc                  C   sP   t jt} t| }|du rtddi}| r|jtdd |S td|d dS )	z3Status-Check: ist diese Browser-Session eingeloggt?Nr  Fr   r   TrW   )r  rW   )r   r   rN   r   r   r	   r   )r   r   r   r"   r"   r#   admin_api_session  s   r  z/admin/api/loginc               
   C   sf  t jpi } | dpd }| dpd}t }td| ddds&tdS zt| W n tj	yL } zt
t||jd	d
fW  Y d}~S d}~ww |rQ|sYt
ddidfS t|||}|szt|d| dd d W n ty   td Y nw td|d|  t
ddd	dfS t||}td| d|  t
d|d}t|| |S )z;Admin-Login. Body: {username, password}. Rate-Limit pro IP.rW   r2   r`   zadmin_login_ip:r  i,  r  r   r   r   Nr&   z#Username und Passwort sind Pflicht.r  zadmin:r  admin_invalidz+Fehler beim Loggen eines Admin-FehlversuchszAdmin-Login fehlgeschlagen (r  zUsername oder Passwort falsch.r  r   zAdmin-Login: z von T)r  rW   )r   rg   rN   r   r   r   r   r   check_rate_limitr  r	   rO   r   verify_admin_record_failed_loginrQ   rM   r  r   r   r   )r}   rW   r`   r   r  r  r   r   r"   r"   r#   admin_api_login  s@   
$ 

r  z/admin/api/logoutc                  C   s2   t jt} t|  tddi}|jtdd |S )Nr  Tr   r   )r   r   rN   r   r   r	   r   )r   r   r"   r"   r#   admin_api_logout  s
   r  z/admin/api/accountsc                  C   s\   t jdddk} ztj| d}W n ty'   td tddidf Y S w td	|iS )
Ninclude_disabledrc  r$  )r  z"Admin: Konten-Liste fehlgeschlagenr&   z$Konten konnten nicht geladen werden.r  accounts)	r   r   rN   r   list_accountsrQ   rM   r  r	   )r  acctsr"   r"   r#   admin_api_accounts_list  s   
r  c                  C   s~  t jpi } | dpd }| dpd pd }| dpd p#d }| dp*d p.d }zt| dp7d}W n ttfyM   tdd	id
f Y S w |sXtddid
fS |dk rdtddid
fS ztj	|||||t j
d}W n1 ty } ztdt|id
fW  Y d }~S d }~w ty   td tddidf Y S w tdt j
 d| d| d| d	 t|||dS )Nri  r2   customer_emaildevice_namenotesinitial_tokensr   r&   z#initial_tokens muss eine Zahl sein.r  zcustomer_name ist Pflicht.z'initial_tokens darf nicht negativ sein.)r  r   z$Admin: create_account fehlgeschlagenz#Konto konnte nicht angelegt werden.r  Admin z: Konto angelegt (r+  z Tokens))r  ri  r  )r   rg   rN   r   r  r)  r(  r	   r   create_accountr   rO   rQ   rM   r  r   )r}   ri  r  r  r  r  r  r  r"   r"   r#   admin_api_accounts_create"  s<   

 
&r  z /admin/api/accounts/<uid>/statusc              
   C   s   t jpi }|dpd }|dvrtddidfS ztj| |t jd W n1 tyA } ztdt	|idfW  Y d }~S d }~w t
yV   td tdd	id
f Y S w td| |dS )Nr  r2   )activer  r  r&   z)status muss active/disabled/expired sein.r  r   z Admin: set_status fehlgeschlagenz#Status konnte nicht gesetzt werden.r  T)r  r  r  )r   rg   rN   r   r	   r   
set_statusr   r(  rO   rQ   rM   r  )r  r}   r  r  r"   r"   r#   admin_api_account_statusD  s   
 
r  z /admin/api/accounts/<uid>/unlockc              
   C   s   z
t j| tjd}W n1 ty& } ztdt|idfW  Y d}~S d}~w ty;   t	d tddidf Y S w t
dtj d	|  d
| d td| |dS )uI   Device-Lock sofort aufheben – für Support-Fälle (z.B. Browser-Crash).r  r&   r  Nz#Admin: unlock_device fehlgeschlagenzEntsperren fehlgeschlagen.r  r  u   : Gerät für z entsperrt (u    Session(s) gelöscht)T)r  r  deleted_sessions)r   unlock_devicer   r   r(  r	   rO   rQ   rM   r  r   )r  deletedr  r"   r"   r#   admin_api_account_unlockU  s    

r  z /admin/api/accounts/<uid>/tokensc              
   C   s   t jpi }z	t|d}W n ttfy!   tddidf Y S w |dp(d p,d}ztj	| ||t j
d}W t|S  tyX } ztdt|idfW  Y d }~S d }~w tym   td	 tdd
idf Y S w )Ndeltar&   zdelta muss eine Ganzzahl sein.r  r   r2   admin_adjustr  z#Admin: adjust_tokens fehlgeschlagenzToken-Anpassung fehlgeschlagen.r  )r   rg   r  rN   r)  r(  r	   r   r   adjust_tokensr   rO   rQ   rM   r  )r  r}   r  r   r   r  r"   r"   r#   admin_api_account_tokense  s&   

 
r  z/admin/api/accounts/<uid>/usagec                 C   sL   z	t j| dd}W n ty   td tddidf Y S w td|iS )N   )r  limitz#Admin: get_usage_log fehlgeschlagenr&   z&Usage-Log konnte nicht geladen werden.r  entries)r   get_usage_logrQ   rM   r  r	   )r  r  r"   r"   r#   admin_api_account_usagey  s   
r  z/admin/api/packagesc                  C   r  )NFr  z#Admin: list_packages fehlgeschlagenr&   z$Pakete konnten nicht geladen werden.r  r  r  r  r"   r"   r#   admin_api_packages_list  r  r  z/admin/api/packages/<key>c              
   C   s   t jpi }|d}|d}z|d urt|nd }W n ttfy.   tddidf Y S w ztj| ||t j	d}W t|S  tyZ } ztdt
|idfW  Y d }~S d }~w tyo   td tddid	f Y S w )
Ncost	is_activer&   zcost muss eine Ganzzahl sein.r  )r  r  r   z$Admin: update_package fehlgeschlagenz'Paket konnte nicht aktualisiert werden.r  )r   rg   rN   r  r)  r(  r	   r   update_packager   rO   rQ   rM   r  )r   r}   r  r  cost_ipkgr  r"   r"   r#   admin_api_package_update  s*   


	 
r  z /admin/api/accounts/<uid>/exportc              
   C   s   zt | }W n1 ty# } ztdt|idfW  Y d}~S d}~w ty8   td tddidf Y S w tdt	j
 d|  d	 ttj|d
dtd}d|jd< d|  d|jd< |S )uq   DSGVO Art. 15 – Recht auf Auskunft.
    Liefert alle zu einem Konto gespeicherten Daten als JSON-Download.
    r&   r  Nz)Admin: export_account_data fehlgeschlagenzExport fehlgeschlagen.r  r  z: DSGVO-Auskunft zu z exportiertFro   )rq   rr   r   zapplication/json; charset=utf-8zContent-Typezattachment; filename="z.json"zContent-Disposition)r   export_account_datar(  r	   rO   rQ   rM   r  r   r   r   r   rg   rt   r   )r  rn   r  r   r"   r"   r#   admin_api_account_export  s    

r  z/admin/api/accounts/<uid>DELETEc              
   C   s~   zt j| tjd}W t|S  ty) } ztdt|idfW  Y d}~S d}~w ty>   t	d tddidf Y S w )us   DSGVO Art. 17 – Recht auf Löschung.
    Anonymisiert das Konto und löscht das komplette Nutzungsprotokoll.
    r  r&   r  Nz$Admin: delete_account fehlgeschlagenu   Löschung fehlgeschlagen.r  )
r   delete_accountr   r   r(  r	   rO   rQ   rM   r  )r  rv  r  r"   r"   r#   admin_api_account_delete  s    
r  z/datenschutzc                   C   r  )uQ   v1.07b: Datenschutzerklärung direkt aus dem Template rendern (DSGVO Art. 15/17).zdatenschutz.htmlr  r"   r"   r"   r#   datenschutz_page  r  r  musterz/api/v1/beispiel-ergebnisc                  C   0   t d } |  stddidfS t| ddddS )	u   Vollständiges Beispiel-ZIP (liegt fertig im muster/-Ordner).
    Keine Authentifizierung nötig – Vorab-Einblick für neue Nutzer.zEEG_Abrechnung_Beispiel.zipr&   zBeispiel-ZIP nicht gefunden.r  ry  TzBeispiel-Ergebnis-EEG.ziprz  _MUSTERr  r	   r   rv  r"   r"   r#   api_beispiel_ergebnis  s   r  z/api/v1/muster/rechnungc                  C   s.   t d } |  stddidfS t| dddS )zMuster-Rechnung als PDF.zRechnung_Beispiel.pdfr&   Muster nicht gefunden.r  application/pdfF)r7  r{  r  rv  r"   r"   r#   api_muster_rechnung  s   r  z/api/v1/muster/sepac                  C   r  )	zMuster-SEPA-Lastschrift-XML.zSEPA_Lastschrift_Beispiel.xmlr&   r  r  zapplication/xmlTzMuster-SEPA-Lastschrift.xmlrz  r  rv  r"   r"   r#   api_muster_sepa     r  z/api/v1/muster/kassabuchc                  C   r  )	zMuster-Kassabuch als XLSX.zKassabuch_Beispiel.xlsxr&   r  r  Aapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheetTzMuster-Kassabuch-EEG.xlsxrz  r  rv  r"   r"   r#   api_muster_kassabuch  s   r  z/api/v1/muster/berichtc                  C   r  )	z$Muster-Gemeinschaftsbericht als PDF.z!Gemeinschaftsbericht_Beispiel.pdfr&   r  r  r  Tz#Muster-Gemeinschaftsbericht-EEG.pdfrz  r  rv  r"   r"   r#   api_muster_bericht  r  r  z/api/v1/cancel-sessionc                  C   sN   t jdd} t| r!zt|  W n ty    td Y nw t	ddiS )zLoescht Tempordner und zerstoert Session.
    Wird vom Wizard aufgerufen, wenn der User abbricht.
    Antwortet immer 200 (Frontend macht den Redirect so oder so).r   r2   z&cancel-session: destroy fehlgeschlagenr  T)
r   r   rN   r   r  rn  rQ   rM   r  r	   r   r"   r"   r#   api_cancel_session	  s   r  z/admin/api/auditc                  C   s~   zt ttjddd} W n ty   d} Y nw ztj| d}W n ty8   t	
d tddidf Y S w td	|iS )
Nr  r  i  )r  z&Admin: list_admin_audit fehlgeschlagenr&   z&Audit-Log konnte nicht geladen werden.r  r  )r  r  r   r   rN   r(  r   list_admin_auditrQ   rM   r  r	   )r  r  r"   r"   r#   admin_api_audit	  s   
r  z/admin/api/smtpc               
   C   sN   t  } t| d | d | d | d | d rdnd| d t| d o"| d d	S )
uF   Gibt die aktuelle SMTP-Konfiguration zurück (Passwort wird maskiert).rY   r[   r]   rW   r`   u   ••••••••r2   rb   )rY   r[   r]   rW   r`   rb   r  )rm   r	   r   )rl   r"   r"   r#   admin_api_smtp_get!	  s   r  c               
   C   s   t jpi } t }| dd}d|v r|d }| dd  }| dd | d|dkr0dnd	 || d
d || dd d}zt| W n" tyo } zt	d t
dt|ddfW  Y d}~S d}~ww t
ddiS )z1Speichert SMTP-Konfiguration in smtp_config.json.r`   r2   u   •r]   r_   rY   r[   465587rW   rb   rd   z+SMTP-Config konnte nicht gespeichert werdenFr  r  Nr  T)r   rg   rm   rN   r   r   rv   rQ   rM   r  r	   rO   )r}   r  pwr]   rn   r  r"   r"   r#   admin_api_smtp_post1	  s*   

"r  z/api/v1/validate-paramsc               
   C   s   t jpi } zddlm} || }W n( ty9 } ztdd tddd| dgd	W  Y d
}~S d
}~ww tt	|dk|d	S )zOPrueft die Vereinsparameter. Erwartet ein JSON-Dict mit den Frontend-Feld-Keys.r   )validate_paramsappzvalidate_params fehlgeschlagenFr2   zFehler bei der Pruefung: )r  rL  )r  rO  N)
r   rg   r`  r  rQ   rK   r   r  r	   r   )r}   r  rO  r  r"   r"   r#   api_validate_paramsN	  s   
r  z/admin/api/test-mailc                  C   sD  ddl } ddlm} ddlm} ddlm} ddlm} t }|d  	 }|d  }|d r7t
|d n|dkr=d	nd
}|d  }	|d }
|d pN|	}|rS|	s\tddddfS tjp`i }|dpg| }|d|f}| }||d< ||d< d|d< | |d< | |d< ||d| d| d|  ddd zY|d kr| j||d!d"}|  |  |  ||	|
 || W d   n1 sw   Y  n#| j||d!d"}||	|
 || W d   n1 sw   Y  W n | jy   tdd#dd$f Y S  | jy3 } ztdd%| d| d&| dd$fW  Y d}~S d}~w | jyR } ztdd'| dd$fW  Y d}~S d}~w typ } ztdd(| dd$fW  Y d}~S d}~w ty } zt d) tdd*| dd$fW  Y d}~S d}~ww td+d,| d-| d.d/S )0u  Sendet eine Test-E-Mail über die server-seitig konfigurierten SMTP-Zugangsdaten.

    SMTP-Konfiguration über Umgebungsvariablen am Server:
        EEG_SMTP_SERVER    – Hostname des SMTP-Servers
        EEG_SMTP_PORT      – Port (Default: 465)
        EEG_SMTP_MODE      – "ssl" oder "starttls" (Default: "ssl")
        EEG_SMTP_USERNAME  – Benutzername / Absenderadresse
        EEG_SMTP_PASSWORD  – Passwort
        EEG_MAIL_DOMAIN    – Domain für From-Adressen (Default: energie-durchdacht.at)

    Erwartet optional JSON: { "to": "empfaenger@example.com" }
    Fehlt "to", wird die Test-Mail an EEG_SMTP_USERNAME gesendet.
    r   N)MIMEMultipart)MIMETextr]   rY   r[   r_   i  iK  rW   r`   rb   Fz_SMTP nicht konfiguriert. Bitte zuerst im Admin-Bereich unter 'System' die SMTP-Daten eintragen.r  r  tozEEG-AssistentFromTou-   [TEST] EEG-Assistent – SMTP-VerbindungstestSubjectz
Message-IDDateuG   Diese Test-E-Mail bestätigt, dass der EEG-Assistent erfolgreich über :z (z5) E-Mails versenden kann.

Keine Aktion erforderlich.plainr=   starttlsra  r  uG   Authentifizierung fehlgeschlagen – Passwort oder Benutzername falsch.r  zVerbindung zu rk  zSMTP-Fehler: zNetzwerkfehler: zTest-Mail unbekannter FehlerzUnbekannter Fehler: TzTest-Mail gesendet von z an r#  )r  rE   )!smtplibemail.utilsutilsemail.mime.multipartr  email.mime.textr  rm   r   r   r  r	   r   rg   rN   
formataddr
make_msgid
formatdateattachrP   SMTPehlor  r  send_messageSMTP_SSLSMTPAuthenticationErrorSMTPConnectErrorSMTPExceptionr  rQ   rM   r  )r  _eur  r  _eu2rl   rS  rR  	smtp_portrT  rU  	smtp_fromr}   to_addr	from_addrr  srvr  r"   r"   r#   api_test_mail`	  s   $
$$
$r  z/api/v1/template/mitgliederc                  C   s0   t d } |  stddidfS t| ddddS )z7Mitglieder-Vorlage herunterladen (aus sepa_templates/).r'  r&   zVorlage nicht gefundenr  r  Trz  )rf  rf   r	   r   r   r"   r"   r#   api_template_mitglieder	  s   r  z/api/v1/template/vorlagec                  C   s,   t } |  stddidfS t| ddddS )z(Rechnungs-Standardvorlage herunterladen.r&   zRechnungsvorlage nicht gefundenr  r  TrU   rz  )DEFAULT_VORLAGErf   r	   r   r   r"   r"   r#   api_template_vorlage	  s   r  r8  )r   )rF   N)r   r~   )r  r  rg   rK   rj   r  r   rd  sysr9  r   r   r  r   r   	functoolsr   pathlibr   flaskr   r   r	   r
   r   r   r   r   werkzeug.utilsr   werkzeug.middleware.proxy_fixr   r   insertdirname__file__session_managerr  r   ensure_readyr  r  wsgi_appurandom
secret_keyr  after_requestr$   errorhandlerr(   basicConfigrD   r   rM   logging.handlersr,   r4   r
  parent_LOG_DIRrb  	_LOG_PATHr  setLevelrG   ry  handlers_already_fhr  r  r  r   rL   rO   rS   V1_DIRrf  r  re   rh  rm   rv   rc  ri  compiler   r   r   r   r   r   r  r%  __annotations__Lockr   r  r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r  r
  r  
IGNORECASEr  r  r  rt  rx  r  bytesr  Handlerr  mathr  r  r  router  r  r  r  r  r  r  r  r  rV  r^  r&  rs  r  r  r  r<  r`  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r"   r"   r"   r#   <module>   s  `(










*
		



b*8

)


) ( &V w @ 

%

 		


	


	

P
