monthly report added

This commit is contained in:
Mark Kors
2025-10-31 09:35:23 +01:00
parent 740bb0f637
commit 69652adfbf
3 changed files with 703 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
#!/usr/bin/env python3
"""
Maandelijkse Declaratie Helper Script
Maakt het genereren en versturen van rapporten eenvoudiger
Werkt op zowel Windows als Linux/Mac
"""
import argparse
import subprocess
import sys
import os
from datetime import datetime, timedelta
import calendar
def get_previous_month():
"""Bereken vorige maand en jaar"""
today = datetime.now()
first_day_current = today.replace(day=1)
last_month = first_day_current - timedelta(days=1)
return last_month.month, last_month.year
def show_header(month, year, email, user=None, summary=False):
"""Toon header informatie"""
print("=" * 50)
print("Maandelijkse Declaratie Generator")
print("=" * 50)
print(f"Periode: {calendar.month_name[month]} {year}")
print(f"Email: {email}")
if user:
print(f"Gebruiker: {user}")
if summary:
print("Modus: Alleen samenvatting")
print("=" * 50)
def run_command(cmd, shell=False):
"""Voer commando uit en toon output"""
try:
if os.name == 'nt': # Windows
# Op Windows, gebruik shell=True voor complexe commando's
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, encoding='utf-8')
else: # Linux/Mac
result = subprocess.run(cmd, shell=shell, capture_output=True, text=True, encoding='utf-8')
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
return result.returncode == 0
except Exception as e:
print(f"Fout bij uitvoeren commando: {e}")
return False
def main():
# Standaard waarden - vorige maand
default_month, default_year = get_previous_month()
parser = argparse.ArgumentParser(
description='Helper script voor maandelijkse declaratie rapporten',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Voorbeelden:
# Rapport vorige maand versturen
python monthly_declaration_helper.py
# Oktober 2024
python monthly_declaration_helper.py -m 10 -y 2024
# Toon rapport vorige maand op scherm
python monthly_declaration_helper.py -d
# Verstuur alleen samenvatting
python monthly_declaration_helper.py -s
# Rapport voor specifieke gebruiker
python monthly_declaration_helper.py -u 04A2CBA2C43C80
"""
)
parser.add_argument('-m', '--month', type=int, default=default_month,
choices=range(1, 13),
help=f'Maand (1-12). Standaard: {default_month} (vorige maand)')
parser.add_argument('-y', '--year', type=int, default=default_year,
help=f'Jaar. Standaard: {default_year}')
parser.add_argument('-u', '--user',
help='Filter op specifieke gebruiker/RFID tag (optioneel)')
parser.add_argument('-e', '--email', default='mark@markkors.nl',
help='Email adres (standaard: mark@markkors.nl)')
parser.add_argument('-s', '--summary', action='store_true',
help='Alleen samenvatting (geen details)')
parser.add_argument('-d', '--display', action='store_true',
help='Toon rapport op scherm (niet versturen)')
args = parser.parse_args()
# Toon header
show_header(args.month, args.year, args.email, args.user, args.summary)
# Database configuratie
db_config = {
'host': '192.168.178.201',
'port': '3307',
'user': 'steve',
'password': 'kS9R*xp17ZwCD@CV&E^N',
'price_user': 'alfen_user',
'price_password': '5uVgr%f%s2P5GR@3q!'
}
print("")
if args.display:
# Toon op scherm
print("Genereer rapport...")
cmd = [
'python', 'steve_monthly_declaration.py',
'--host', db_config['host'],
'--port', db_config['port'],
'--user', db_config['user'],
'--password', db_config['password'],
'--price-user', db_config['price_user'],
'--price-password', db_config['price_password'],
'--month', str(args.month),
'--year', str(args.year)
]
if args.user:
cmd.extend(['--user', args.user])
if args.summary:
cmd.append('--summary')
success = run_command(cmd)
else:
# Verstuur per email
print("Genereer en verstuur rapport...")
cmd = [
'python', 'send_monthly_declaration.py',
'--month', str(args.month),
'--year', str(args.year),
'--email', args.email
]
if args.user:
cmd.extend(['--user', args.user])
if args.summary:
cmd.append('--summary')
success = run_command(cmd)
print("")
if success:
print("✓ Klaar!")
else:
print("✗ Er is een fout opgetreden")
sys.exit(1)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,105 @@
#!/usr/bin/env python3
import os
import subprocess
import smtplib
import argparse
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from datetime import datetime
import calendar
os.environ['PYTHONIOENCODING'] = 'utf-8'
# Parse command line arguments
parser = argparse.ArgumentParser(description='Genereer en verstuur maandelijkse declaratie rapport')
parser.add_argument('--month', type=int, required=True, choices=range(1, 13),
help='Maand (1-12)')
parser.add_argument('--year', type=int, required=True,
help='Jaar (bijv. 2024)')
parser.add_argument('--user', help='Filter op specifieke RFID tag/gebruiker (optioneel)')
parser.add_argument('--email', default='mark@markkors.nl',
help='Email adres (default: mark@markkors.nl)')
parser.add_argument('--summary', action='store_true',
help='Stuur alleen samenvatting zonder transactie details')
args = parser.parse_args()
# Configuratie
TO_EMAIL = args.email
FROM_EMAIL = "laadpaal@raspberry.local"
SUBJECT = f"Declaratie Laadpaal - {calendar.month_name[args.month]} {args.year}"
# SMTP configuratie
SMTP_SERVER = "192.168.178.201"
SMTP_PORT = 25
# Genereer rapport commando
cmd = [
'python', 'steve_monthly_declaration.py',
'--host', '192.168.178.201',
'--port', '3307',
'--user', 'steve',
'--password', 'kS9R*xp17ZwCD@CV&E^N',
'--price-user', 'alfen_user',
'--price-password', '5uVgr%f%s2P5GR@3q!',
'--month', str(args.month),
'--year', str(args.year)
]
# Voeg optionele parameters toe
if args.user:
cmd.extend(['--user', args.user])
SUBJECT += f" - Gebruiker: {args.user}"
if args.summary:
cmd.append('--summary')
try:
# Genereer rapport
print(f"📊 Genereer declaratie rapport voor {calendar.month_name[args.month]} {args.year}...")
report = subprocess.check_output(cmd, encoding='utf-8')
except subprocess.CalledProcessError as e:
print(f"✗ Fout bij genereren rapport: {e}")
exit(1)
# Email samenstellen
msg = MIMEMultipart('alternative')
msg['From'] = FROM_EMAIL
msg['To'] = TO_EMAIL
msg['Subject'] = SUBJECT
# Plain text versie
text_part = MIMEText(report, 'plain', 'utf-8')
msg.attach(text_part)
# HTML versie voor betere leesbaarheid in email clients
html_report = f"""
<html>
<head>
<style>
body {{ font-family: 'Courier New', monospace; font-size: 12px; }}
pre {{ white-space: pre-wrap; word-wrap: break-word; }}
.header {{ font-weight: bold; color: #2c3e50; }}
.total {{ font-weight: bold; background-color: #f8f9fa; padding: 5px; }}
</style>
</head>
<body>
<pre>{report.replace('DECLARATIE RAPPORT LAADTRANSACTIES', '<span class="header">DECLARATIE RAPPORT LAADTRANSACTIES</span>')
.replace('TOTAAL OVERZICHT', '<span class="header">TOTAAL OVERZICHT</span>')
.replace('TOTALE KOSTEN:', '<span class="total">TOTALE KOSTEN:</span>')}</pre>
</body>
</html>
"""
html_part = MIMEText(html_report, 'html', 'utf-8')
msg.attach(html_part)
# Verstuur email
try:
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
server.send_message(msg)
print(f"✔ Declaratie rapport verzonden naar {TO_EMAIL}")
print(f" Periode: {calendar.month_name[args.month]} {args.year}")
if args.user:
print(f" Gebruiker: {args.user}")
except Exception as e:
print(f"✗ Fout bij verzenden: {e}")
exit(1)

View File

@@ -0,0 +1,440 @@
#!/usr/bin/env python3
"""
SteVe Monthly Declaration Report Generator
Genereert maandelijkse declaratie rapporten van laadtransacties
"""
import mysql.connector
from datetime import datetime, timedelta
import argparse
import sys
import io
import os
from typing import List, Dict, Optional
import calendar
from collections import defaultdict
os.environ['PYTHONIOENCODING'] = 'utf-8'
# Forceer UTF-8 encoding voor stdout/stderr op Windows
if sys.platform == 'win32':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
class MonthlyDeclarationReporter:
def __init__(self, host: str, database: str, user: str, password: str, port: int = 3306,
price_host: str = None, price_port: int = None, price_user: str = None, price_password: str = None):
"""Initialiseer database connectie"""
self.config = {
'host': host,
'database': database,
'user': user,
'password': password,
'port': port
}
# Prijzen database config (gebruik SteVe config als fallback)
self.price_config = {
'host': price_host or host,
'port': price_port or port,
'user': price_user or user,
'password': price_password or password
}
self.conn = None
self.cursor = None
self.price_conn = None
self.price_cursor = None
def connect(self):
"""Maak verbinding met de database"""
try:
self.conn = mysql.connector.connect(**self.config)
self.cursor = self.conn.cursor(dictionary=True)
print(f"✓ Verbonden met database {self.config['database']} op {self.config['host']}")
except mysql.connector.Error as err:
print(f"✗ Database fout: {err}")
sys.exit(1)
def disconnect(self):
"""Sluit database verbinding"""
if self.cursor:
self.cursor.close()
if self.conn:
self.conn.close()
if hasattr(self, 'price_cursor') and self.price_cursor:
self.price_cursor.close()
if hasattr(self, 'price_conn') and self.price_conn:
self.price_conn.close()
def get_monthly_transactions(self, year: int, month: int, user_filter: Optional[str] = None) -> List[Dict]:
"""Haal alle transacties op voor een specifieke maand"""
# Bereken eerste en laatste dag van de maand
first_day = datetime(year, month, 1)
last_day = datetime(year, month, calendar.monthrange(year, month)[1], 23, 59, 59)
query = """
SELECT
t.transaction_pk,
t.id_tag,
t.start_timestamp,
t.stop_timestamp,
t.start_value,
t.stop_value,
t.stop_reason,
cb.charge_box_id,
cb.description as charge_box_description,
c.connector_id
FROM transaction t
JOIN connector c ON t.connector_pk = c.connector_pk
JOIN charge_box cb ON c.charge_box_id = cb.charge_box_id
WHERE t.start_timestamp >= %s AND t.start_timestamp <= %s
AND t.stop_timestamp IS NOT NULL
"""
params = [first_day, last_day]
if user_filter:
query += " AND t.id_tag = %s"
params.append(user_filter)
query += " ORDER BY t.start_timestamp ASC"
self.cursor.execute(query, params)
return self.cursor.fetchall()
def get_user_info(self, id_tag: str) -> Optional[Dict]:
"""Haal gebruikersinformatie op"""
query = """
SELECT u.first_name, u.last_name, u.e_mail
FROM user u
JOIN user_ocpp_tag uot ON u.user_pk = uot.user_pk
JOIN ocpp_tag ot ON uot.ocpp_tag_pk = ot.ocpp_tag_pk
WHERE ot.id_tag = %s
"""
self.cursor.execute(query, (id_tag,))
return self.cursor.fetchone()
def get_price(self, timestamp: datetime, provider: str, price_db: str) -> Optional[float]:
"""Haal prijs op voor een specifieke timestamp en provider"""
# Maak verbinding met prijzen database indien nodig
if not hasattr(self, 'price_conn') or self.price_conn is None:
try:
price_config = self.price_config.copy()
price_config['database'] = price_db
self.price_conn = mysql.connector.connect(**price_config)
self.price_cursor = self.price_conn.cursor(dictionary=True)
except mysql.connector.Error:
return None
# Round timestamp naar het uur
hour_timestamp = timestamp.replace(minute=0, second=0, microsecond=0)
# Probeer eerst dynamic_price_data
query = """
SELECT price
FROM dynamic_price_data
WHERE datetime = %s AND provider_code = %s
LIMIT 1
"""
try:
self.price_cursor.execute(query, (hour_timestamp, provider))
result = self.price_cursor.fetchone()
if result:
return float(result['price'])
except mysql.connector.Error:
pass
# Zo niet, probeer dynamic_price_data_tommorow (alleen NextEnergy)
if provider == 'NE':
query = """
SELECT price
FROM dynamic_price_data_tommorow
WHERE datetime = %s
LIMIT 1
"""
try:
self.price_cursor.execute(query, (hour_timestamp,))
result = self.price_cursor.fetchone()
if result:
return float(result['price'])
except mysql.connector.Error:
pass
return None
def calculate_transaction_cost(self, transaction: Dict, provider: str, price_db: str) -> Dict:
"""Bereken de kosten voor een transactie"""
start_time = transaction['start_timestamp']
stop_time = transaction['stop_timestamp']
start_val = float(transaction['start_value']) if transaction['start_value'] else 0
stop_val = float(transaction['stop_value']) if transaction['stop_value'] else 0
total_kwh = stop_val - start_val
# Bereken kosten per uur
current_hour = start_time.replace(minute=0, second=0, microsecond=0)
total_cost = 0
hours_with_price = 0
hours_without_price = 0
while current_hour <= stop_time:
# Bereken verbruik in dit uur
hour_start = max(current_hour, start_time)
hour_end = min(current_hour + timedelta(hours=1), stop_time)
if hour_end > hour_start:
# Bereken fractie van het uur
hour_fraction = (hour_end - hour_start).total_seconds() / 3600
hour_kwh = total_kwh * hour_fraction * 3600 / (stop_time - start_time).total_seconds()
# Haal prijs op
price = self.get_price(current_hour, provider, price_db)
if price is not None:
hour_cost = hour_kwh * price
total_cost += hour_cost
hours_with_price += 1
else:
hours_without_price += 1
current_hour += timedelta(hours=1)
return {
'total_kwh': total_kwh,
'total_cost': total_cost,
'hours_with_price': hours_with_price,
'hours_without_price': hours_without_price,
'avg_price': total_cost / total_kwh if total_kwh > 0 and hours_with_price > 0 else 0
}
def generate_monthly_report(self, year: int, month: int, provider: str = 'NE',
price_db: str = 'alfen', user_filter: Optional[str] = None,
format_type: str = 'detailed'):
"""Genereer maandelijks declaratie rapport"""
transactions = self.get_monthly_transactions(year, month, user_filter)
if not transactions:
print(f"Geen transacties gevonden voor {calendar.month_name[month]} {year}")
return
# Header
print("=" * 100)
print(f"DECLARATIE RAPPORT LAADTRANSACTIES")
print(f"Periode: {calendar.month_name[month]} {year}")
print("=" * 100)
# Groepeer transacties per gebruiker
user_transactions = defaultdict(list)
for trans in transactions:
user_transactions[trans['id_tag']].append(trans)
# Totalen voor het hele rapport
grand_total_sessions = 0
grand_total_kwh = 0
grand_total_cost = 0
grand_total_duration = timedelta()
# Print per gebruiker
for id_tag, user_trans in sorted(user_transactions.items()):
# Gebruikersinfo
user_info = self.get_user_info(id_tag)
if user_info:
print(f"\nGebruiker: {user_info['first_name']} {user_info['last_name']}")
print(f"Email: {user_info['e_mail']}")
print(f"RFID Tag: {id_tag}")
print("-" * 100)
if format_type == 'detailed':
# Gedetailleerde tabel
print(f"{'Datum':<12} {'Start':<6} {'Einde':<6} {'Duur':<8} "
f"{'kWh':>8} {'Kosten':>10} {'Gem. prijs':>12} {'Laadpaal':<20}")
print("-" * 100)
user_total_sessions = 0
user_total_kwh = 0
user_total_cost = 0
user_total_duration = timedelta()
for trans in sorted(user_trans, key=lambda x: x['start_timestamp']):
# Bereken kosten
cost_info = self.calculate_transaction_cost(trans, provider, price_db)
# Bereken duur
duration = trans['stop_timestamp'] - trans['start_timestamp']
hours = int(duration.total_seconds() // 3600)
minutes = int((duration.total_seconds() % 3600) // 60)
duration_str = f"{hours}:{minutes:02d}"
# Update totalen
user_total_sessions += 1
user_total_kwh += cost_info['total_kwh']
user_total_cost += cost_info['total_cost']
user_total_duration += duration
if format_type == 'detailed':
# Print transactie regel
print(f"{trans['start_timestamp'].strftime('%d-%m-%Y'):<12} "
f"{trans['start_timestamp'].strftime('%H:%M'):<6} "
f"{trans['stop_timestamp'].strftime('%H:%M'):<6} "
f"{duration_str:<8} "
f"{cost_info['total_kwh']:>8.2f} "
f"{cost_info['total_cost']:>9.2f} "
f"{cost_info['avg_price']:>11.4f} "
f"{trans['charge_box_id'][:19]:<20}")
if cost_info['hours_without_price'] > 0:
print(f" * Waarschuwing: {cost_info['hours_without_price']} uur zonder prijsdata")
# Gebruiker totalen
print("-" * 100)
total_hours = int(user_total_duration.total_seconds() // 3600)
total_minutes = int((user_total_duration.total_seconds() % 3600) // 60)
print(f"Totaal gebruiker: {user_total_sessions} sessies | "
f"{total_hours}:{total_minutes:02d} uur | "
f"{user_total_kwh:.2f} kWh | "
f"{user_total_cost:.2f}")
if user_total_kwh > 0:
avg_price_user = user_total_cost / user_total_kwh
print(f"Gemiddelde prijs: € {avg_price_user:.4f} per kWh")
# Update grand totalen
grand_total_sessions += user_total_sessions
grand_total_kwh += user_total_kwh
grand_total_cost += user_total_cost
grand_total_duration += user_total_duration
# Grand totalen
print("\n" + "=" * 100)
print("TOTAAL OVERZICHT")
print("=" * 100)
total_hours = int(grand_total_duration.total_seconds() // 3600)
total_minutes = int((grand_total_duration.total_seconds() % 3600) // 60)
print(f"Aantal gebruikers: {len(user_transactions)}")
print(f"Totaal aantal sessies: {grand_total_sessions}")
print(f"Totale laadtijd: {total_hours}:{total_minutes:02d} uur")
print(f"Totaal verbruik: {grand_total_kwh:.2f} kWh")
print(f"TOTALE KOSTEN: € {grand_total_cost:.2f}")
if grand_total_kwh > 0:
avg_price_total = grand_total_cost / grand_total_kwh
print(f"Gemiddelde prijs: € {avg_price_total:.4f} per kWh")
# Statistieken
print(f"\nGemiddeld per sessie:")
if grand_total_sessions > 0:
avg_kwh = grand_total_kwh / grand_total_sessions
avg_cost = grand_total_cost / grand_total_sessions
avg_duration = grand_total_duration / grand_total_sessions
avg_hours = int(avg_duration.total_seconds() // 3600)
avg_minutes = int((avg_duration.total_seconds() % 3600) // 60)
print(f" Verbruik: {avg_kwh:.2f} kWh")
print(f" Kosten: € {avg_cost:.2f}")
print(f" Duur: {avg_hours}:{avg_minutes:02d} uur")
print("\n" + "=" * 100)
print(f"Rapport gegenereerd op: {datetime.now().strftime('%d-%m-%Y %H:%M')}")
print(f"Energieprovider: {provider}")
print("=" * 100)
def main():
parser = argparse.ArgumentParser(
description='SteVe Monthly Declaration Report Generator',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Voorbeelden:
# Genereer rapport voor oktober 2024
python3 steve_monthly_declaration.py --host 192.168.178.201 --port 3307 --password 'geheim' --month 10 --year 2024
# Genereer rapport voor specifieke gebruiker in november 2024
python3 steve_monthly_declaration.py --host 192.168.178.201 --port 3307 --password 'geheim' --month 11 --year 2024 --user 04A2CBA2C43C80
# Gebruik aparte credentials voor prijzen database
python3 steve_monthly_declaration.py --host 192.168.178.201 --port 3307 --password 'geheim' \\
--price-user alfen_user --price-password 'geheim2' --month 10 --year 2024
# Genereer beknopt rapport (zonder details per transactie)
python3 steve_monthly_declaration.py --host 192.168.178.201 --port 3307 --password 'geheim' --month 10 --year 2024 --summary
"""
)
# Database opties
parser.add_argument('--host', default='localhost', help='Database host (default: localhost)')
parser.add_argument('--port', type=int, default=3306, help='Database poort (default: 3306)')
parser.add_argument('--database', default='stevedb', help='Database naam (default: stevedb)')
parser.add_argument('--user', default='steve', help='Database gebruiker (default: steve)')
parser.add_argument('--password', required=True, help='Database wachtwoord')
# Maand/jaar selectie
parser.add_argument('--month', type=int, required=True, choices=range(1, 13),
help='Maand (1-12)')
parser.add_argument('--year', type=int, required=True,
help='Jaar (bijv. 2024)')
# Filter opties
parser.add_argument('--rfid', '--user', dest='user_filter',
help='Filter op specifieke RFID tag/gebruiker')
parser.add_argument('--summary', action='store_true',
help='Toon alleen samenvattingen (geen transactie details)')
# Prijzen opties
parser.add_argument('--provider', default='NE',
help='Energie provider code (default: NE voor NextEnergy)')
parser.add_argument('--price-db', default='alfen',
help='Database naam voor prijzen (default: alfen)')
parser.add_argument('--price-host', default=None,
help='Database host voor prijzen (default: zelfde als --host)')
parser.add_argument('--price-port', type=int, default=None,
help='Database poort voor prijzen (default: zelfde als --port)')
parser.add_argument('--price-user', default=None,
help='Database gebruiker voor prijzen (default: zelfde als --user)')
parser.add_argument('--price-password', default=None,
help='Database wachtwoord voor prijzen (default: zelfde als --password)')
args = parser.parse_args()
# Maak reporter object
reporter = MonthlyDeclarationReporter(
host=args.host,
database=args.database,
user=args.user,
password=args.password,
port=args.port,
price_host=args.price_host,
price_port=args.price_port,
price_user=args.price_user,
price_password=args.price_password
)
try:
reporter.connect()
# Bepaal format type
format_type = 'summary' if args.summary else 'detailed'
# Genereer rapport
reporter.generate_monthly_report(
year=args.year,
month=args.month,
provider=args.provider,
price_db=args.price_db,
user_filter=args.user_filter,
format_type=format_type
)
except KeyboardInterrupt:
print("\n\nAfgebroken door gebruiker")
sys.exit(0)
except Exception as e:
print(f"✗ Fout: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
finally:
reporter.disconnect()
if __name__ == '__main__':
main()