monthly report added
This commit is contained in:
440
declaratie/steve_monthly_declaration.py
Normal file
440
declaratie/steve_monthly_declaration.py
Normal 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()
|
||||
Reference in New Issue
Block a user