Kapitel 7 – Verarbeitung von Ladevorgängen
Ziel dieses Kapitels
Nachdem Stammdaten und Vertragsinformationen erfolgreich aus Gridware nach ERPNext synchronisiert wurden, bildet die Verarbeitung der Ladevorgänge den nächsten zentralen Schritt innerhalb der ERPNext-Lösung.
Jeder einzelne Ladevorgang repräsentiert die Nutzung eines Ladepunkts und bildet die kleinste abrechnungsrelevante Einheit des Systems. Erst durch die anschließende Aggregation mehrerer Ladevorgänge innerhalb eines Abrechnungszeitraums entstehen kaufmännische Belege wie Kunden- oder Lieferantenabrechnungen.
Dieses Kapitel beschreibt den Aufbau des Gridware Usage Doctypes sowie dessen Rolle innerhalb des gesamten Abrechnungsprozesses.
7.1 Fachlicher Hintergrund
Ein Gridware Usage repräsentiert einen einzelnen Ladevorgang und bildet die kleinste abrechnungsrelevante Einheit innerhalb des Systems.
Jeder Ladevorgang entsteht ausschließlich in Gridware und wird anschließend vollständig nach ERPNext synchronisiert. Der Gridware Usage stellt dabei das technische und fachliche Abbild eines einzelnen Ladevorgangs dar.
Im Gegensatz zu vielen Integrationen werden die Daten nicht innerhalb von LadevorgängenERPNext viaberechnet API-Synchronisationoder
Dokumentationnachträglich zurangereichert. Bereits beim Abruf aus Gridware enthält der Datensatz sämtliche relevanten Informationen, die für die spätere kaufmännische Verarbeitung vonbenötigt werden.
Hierzu gehören unter anderem:
die zugehörigen Vertragsinformationen,
der Contract Type,
der zahlende Kunde,
der beteiligte Lieferant,
sämtliche Preisinformationen,
die Verbrauchswerte,
sowie weitere technische und kaufmännische Informationen.
ERPNext übernimmt diese Informationen unverändert und verwendet sie als Grundlage für die spätere Aggregation und Rechnungsstellung.
Der Gridware Usage Datenbildet überdamit die GridwareBasis APIfür:
Customer Usage Data
Supplier Usage Data
Kundenabrechnungen
Lieferantenabrechnungen
Auswertungen und Reports
1.7.2 Workflow-ÜberblickArchitekturprinzip
Der Gridware Usage wurde bewusst als Spiegelbild der Gridware-Daten entwickelt.
Das bedeutet, dass ERPNext keine fachlichen Informationen wie Preise, Vertragszuordnungen oder beteiligte Parteien selbst berechnet. Stattdessen werden diese Informationen bereits vollständig von Gridware bereitgestellt und unverändert übernommen.
7.3 Lebenszyklus eines Ladevorgangs
Die Verarbeitung eines Ladevorgangs erfolgt in mehreren Schritten.
Gridware API
↓
Gridware Usage (Status: Complete)
↓
Auto-Fetch + Aggregation
↓
Customer Usage Data
/↓
Supplier Usage Data
↓
Sales Invoice / Purchase Invoice
Nachdem der Ladevorgang aus Gridware synchronisiert wurde, wird er den entsprechenden Kunden- und Lieferantenaggregationen zugeordnet.
Erst diese Aggregationen bilden die Grundlage für die spätere Rechnungsstellung.
2.7.4 Aufbau des Gridware Usage - Das RohformatDoctypes
Der Gridware Usage Doctype gliedert sich in mehrere logisch getrennte Bereiche. Gemeinsam bilden sie sämtliche technischen und kaufmännischen Informationen eines einzelnen Ladevorgangs ab.
Usage Information
Dieser Bereich enthält die grundlegenden Informationen zum Ladevorgang.
Gespeichert werden unter anderem:
UUID
Gridware Usage ID
Status
Startzeitpunkt
Endzeitpunkt
Dauer des Ladevorgangs
Die Gridware Usage ID dient als eindeutiger technischer Identifikator zwischen Gridware und ERPNext.
Der Status wird direkt aus Gridware übernommen und beschreibt den aktuellen Bearbeitungszustand des Ladevorgangs.
Consumption Information
Dieser Bereich beschreibt den tatsächlichen Energieverbrauch.
Hier werden unter anderem gespeichert:
Zählerstand zu Beginn (Wh / kWh)
Zählerstand am Ende (Wh / kWh)
Verbrauchte Energiemenge
Rental Object Display Name
Diese Informationen dienen später als Grundlage für die Verbrauchsauswertung und Rechnungsstellung.
Usage Price Details
Im Bereich Quelle:Usage Price Details werden sämtliche von Gridware berechneten Kosten übernommen.becharged/becharged/doctype/gridware_usage/gridware_usage.py
Status-Lebenszyklus
Je
1.nach PendingVertragsmodell └─können Rohformatfolgende Kostenarten vorhanden sein:
Energiekosten
Zeitkosten
Parkkosten
Reservierungskosten
Fixkosten
Sowohl Netto- als auch Bruttopreise werden gespeichert.
ERPNext übernimmt diese Werte unverändert. Eine erneute Preisberechnung innerhalb ERPNext erfolgt nicht.
User Information
Dieser Bereich beschreibt den Nutzer des Ladevorgangs.
Hierzu gehören unter anderem:
Usage Medium
Usage Medium Label
Paying Customer
Person ID
Organization ID
Gridware Party
User References
Dadurch kann jeder Ladevorgang eindeutig einer Person oder Organisation zugeordnet werden.
Über die Gridware Party erfolgt die Verknüpfung mit gridware_usage_idden └─kaufmännischen DetailsStammdaten innerhalb ERPNext.
Authorizing Contracts
Dieser Bereich stellt die Verbindung zwischen dem Ladevorgang und dem zugrunde liegenden Vertrag her.
Gespeichert werden asyncunter abgerufenanderem:
Gridware CompleteContract
Contract verfügbarNumber
Contract └─Type
Contract Name
Gridware Supplier Party
Supplier
Dadurch erhält jeder Ladevorgang sämtliche Informationen, die für Aggregationdie 3.spätere Failedkaufmännische └─Verarbeitung API-Fehlererforderlich beimsind.
Der └─Vertrag Wirdbestimmt täglichunter automatischanderem:
welches
WichtigeVertragsmodell Felder
gilt,
welcher Lieferant beteiligt ist,
welche Kunden berücksichtigt werden,
welche Billing Rules angewendet werden.
Payment Information
Im letzten Abschnitt werden die Zahlungsinformationen gespeichert.
Aktuell wird insbesondere die Payment Method Reference ID übernommen.
Dieser Bereich bildet die Grundlage für zukünftige Erweiterungen der Zahlungsabwicklung und dient der eindeutigen Zuordnung der verwendeten Zahlungsmethode.
7.5 Verwendung innerhalb der ERPNext-Lösung
Der Gridware Usage dient ausschließlich als technische Datengrundlage.
Er wird selbst nicht direkt fakturiert.
Stattdessen bildet er die Grundlage für die weitere kaufmännische Verarbeitung.
Mehrere Gridware Usages werden innerhalb einer Billing Period zusammengefasst und anschließend zu folgenden Objekten aggregiert:
Customer Usage Data
Supplier Usage Data
Diese Aggregationen bilden wiederum die Grundlage für die Erstellung von Kunden- und Lieferantenrechnungen.
7.6 Abgrenzung zu Customer Usage Data und Supplier Usage Data
Es ist wichtig, zwischen dem Gridware Usage und den späteren Aggregationsobjekten zu unterscheiden.
Einzelner Ladevorgang |
|
Technisches Abbild aus Gridware |
|
direkt aus Gridware übernommen |
|
Keine Rechnungsstellung |
usage_price_payment_net_amountusage_price_payment_amountrental_object_display_nameuser_usage_medium_labelKostenaufschlüsselung
Während (ausder API)
total_fix_costs_excl_vat # Verbindungsgebühr netto
total_fix_costs_incl_vat # Verbindungsgebühr brutto
total_energy_costs_excl_vat # €/kWh * Verbrauch (netto)
total_energy_costs_incl_vat # €/kWh * Verbrauch (brutto)
total_time_costs_excl_vat # €/min * Dauer (netto)
total_time_costs_incl_vat # €/min * Dauer (brutto)
total_parking_costs_excl_vat # Standgebühr (netto)
total_parking_costs_incl_vat # Standgebühr (brutto)
3. Auto-Fetch Mechanismus
Quelle: becharged/becharged/doctype/customer_usage_data/customer_usage_data.py
Was ist Auto-Fetch?
Beim Öffnen von den CustomerGridware Usage Dataoderunveränderten Supplier Usage Data werden automatisch alle zugehörigen Complete-Status Usages abgerufen.
Ablauf
class CustomerUsageData(BaseUsageData):
def validate(self):
# 1. Auto-fetch
self.auto_fetch_completed_usages()
# 2. Aggregation (hinzufügen zu usage_details Tabelle)
self.aggregate_completed_usages()
# 3. Berechnung der Summen
self.calculate_totals()
# 4. Status setzen
self.set_status()
Auto-Fetch Bedingungen
def auto_fetch_completed_usages(self):
# Nur wenn gesetzt:
if not self.customer or not self.billing_period:
return
# Nicht wenn _completed_usages bereits extern gesetzt (Bulk-API)
if hasattr(self, '_completed_usages'):
return
# Query:
usages = get_customer_usages(
customer=self.customer,
billing_period=self.billing_period,
exclude_usage_ids=[bereits_aggregierte_usages]
)
self._completed_usages = usages # Wird dann aggregiert
4. Query-Logik für Customer Usages
Quelle: becharged/becharged/utils.py (Funktion: get_customer_usages)
Abfrage-Struktur
def get_customer_usages(
customer: str = None, # Customer Name
billing_period: str = None, # "MM/YYYY" z.B. "01/2026"
from_date: str = None, # Optionaler Start
to_date: str = None, # Optionales Ende
exclude_usage_ids: List[str] = None # Bereits aggregierte
) -> List[Dict]:
SQL-Logik (vereinfacht)
SELECT
gu.name,
gu.gridware_contract,
gu.end,
gc.contract_type,
gc.vat,
gu.status
FROM `tabGridware Usage` gu
INNER JOIN `tabGridware Contract` gc
ON gu.gridware_contract = gc.name
INNER JOIN `tabGridware Contract Customer` gcc
ON (gcc.parent = gc.name
AND gcc.gridware_party = customer.custom_gridware_party)
WHERE
gu.status IN ("Complete", "Pending")
AND NOT gu.name IN (SELECT gridware_usage FROM `tabCustomer Usage Detail`)
AND MONTH(gu.end) = billing_month
AND YEAR(gu.end) = billing_year
ORDER BY gu.end
Filtermechanismus
CompletePendingcustom_gridware_partyendCustomer Usage Detail5. Aggregation - Vom Usage zur Usage Data
Quelle: becharged/becharged/utils.py (Klasse: BaseUsageData)
Was ist Aggregation?
Eine Aggregation ist das HinzufügenUrsprung eines darstellt, GridwareLadevorgangs Usagezu einem Customer Usage Data Datensatz, indem eine Zeile in der usage_details Tabelle erstellt wird.
Aggregations-Prozess
def aggregate_completed_usages(self):
"""Nur ausgeführt wenn _completed_usages gesetzt ist"""
for usage_data in self._completed_usages:
usage = frappe.get_doc("Gridware Usage", usage_data["name"])
contract = frappe.get_doc("Gridware Contract", usage.gridware_contract)
# Item-Mapping: Contract Type + VAT → Item Code
item_code = get_item_code_from_mapping(
contract.contract_type,
contract.vat
)
if not item_code:
frappe.log_error(f"No mapping for {contract.contract_type} + {contract.vat}%")
continue
# Zeile hinzufügen
self.append("usage_details", {
"gridware_usage": usage.name,
"gridware_contract": usage.gridware_contract,
"contract_type": contract.contract_type,
"vat": contract.vat,
"item_code": item_code,
"usage_start": usage.start,
"usage_end": usage.end,
"duration": usage.duration,
"consumption_kwh": usage.consumption_value_in_kwh,
"price_excl_vat": usage.usage_price_payment_net_amount,
"price_incl_vat": usage.usage_price_payment_amount,
"rental_object_display_name": usage.rental_object_display_name,
# Kostenaufschlüsselung:
"usage_price_total_fix_costs_excl_vat": usage.usage_price_total_fix_costs_excl_vat,
"usage_price_total_fix_costs_incl_vat": usage.usage_price_total_fix_costs_incl_vat,
"usage_price_total_energy_costs_excl_vat": usage.usage_price_total_energy_costs_excl_vat,
"usage_price_total_energy_costs_incl_vat": usage.usage_price_total_energy_costs_incl_vat,
"usage_price_total_time_costs_excl_vat": usage.usage_price_total_time_costs_excl_vat,
"usage_price_total_time_costs_incl_vat": usage.usage_price_total_time_costs_incl_vat,
"usage_price_total_parking_costs_excl_vat": usage.usage_price_total_parking_costs_excl_vat,
"usage_price_total_parking_costs_incl_vat": usage.usage_price_total_parking_costs_incl_vat,
})
Duplikat-Schutz
def validate_usages_not_aggregated(usage_list: List[str], detail_table_name: str):
"""
Prüft, dass keine Usage bereits aggregiert wurde.
detail_table_name: "Customer Usage Detail" oder "Supplier Usage Detail"
"""
existing = frappe.qb.from_(detail_table)
.select(detail.gridware_usage)
.where(detail.gridware_usage.isin(usage_list))
.run(as_dict=True)
if existing:
frappe.throw(
f"These usages are already aggregated: {existing}"
)
6. Berechnung von Gesamtzahlen
Quelle: becharged/becharged/utils.py (Methode: calculate_totals)
Nach der Aggregation werden Summen berechnet:
def calculate_totals(self):
"""Summiert alle Werte aus usage_details"""
self.total_consumption = 0
self.total_usage_time = 0
self.total_charging_sessions = 0
self.total_price_excl_vat = 0
self.total_price_incl_vat = 0
self.total_fix_costs_excl_vat = 0
self.total_fix_costs_incl_vat = 0
# ... weitere Kosten-Felder
for detail in self.usage_details:
if not detail.gridware_usage:
continue
# Verbrauch & Zeit
self.total_consumption += detail.consumption_kwh
self.total_usage_time += detail.duration
self.total_charging_sessions += 1
# Preise
self.total_price_excl_vat += detail.price_excl_vat
self.total_price_incl_vat += detail.price_incl_vat
# Kostenaufschlüsselung
self.total_fix_costs_excl_vat += detail.usage_price_total_fix_costs_excl_vat
self.total_energy_costs_excl_vat += detail.usage_price_total_energy_costs_excl_vat
# ... weitere Kosten
# Eindeutige Ladesäulen zählen
self.total_charging_points = len(
set(d.rental_object_display_name for d in self.usage_details)
)
7. Status-Management
Status-Workflow
Uninvoiced
↓
[Knopf: Create Sales/Purchase Invoice]
↓
Draft Invoice
↓
[Submit Invoice]
↓
Invoiced
Automatische Status-Synchronisation
def _set_status_from_invoice(self, invoice_doctype: str, link_field: str):
"""
Status abgeleitet vom verknüpften Invoice
"""
invoice = frappe.db.get_value(
invoice_doctype, # "Sales Invoice" oder "Purchase Invoice"
{link_field: self.name}, # "custom_customer_usage_data" etc.
["name", "docstatus"],
as_dict=True
)
if invoice:
if invoice.docstatus == 0:
self.status = "Draft Invoice"
elif invoice.docstatus == 1:
self.status = "Invoiced"
elif invoice.docstatus == 2:
self.status = "Cancelled Invoice"
else:
self.status = "Uninvoiced"
Diese Methode wird aufgerufen beim:
8.enthalten Customer Usage Data vs.und Supplier Usage Data
die kaufmännisch aufbereiteten Daten für die spätere Abrechnung.
7.7 Zusammenfassung
Der Gridware Usage bildet die technische Grundlage der gesamten Abrechnungslösung.
Jeder Ladevorgang wird vollständig aus Gridware übernommen und unverändert innerhalb ERPNext gespeichert. Dadurch bleibt Gridware das führende System für sämtliche Nutzungsdaten, während ERPNext die Rolle des kaufmännischen Verarbeitungssystems übernimmt.
Die eigentliche Geschäftslogik beginnt erst im nächsten Schritt mit der Aggregation mehrerer Gridware Usages zu Customer Usage Data
#und Query-Beispiel
from becharged.becharged.utils import get_customer_usages
usages = get_customer_usages(
customer="Customer A",
billing_period="01/2026"
)
# Erstelle/Update
cud = frappe.get_doc({
"doctype": "Customer Usage Data",
"customer": "Customer A",
"billing_period": "01/2026",
"month": 1,
"year": 2026
})
cud._completed_usages = usages # Auto-fetch wird übersprungen
cud.save() # validate() aggregiert
Supplier Usage Data
from becharged.becharged.utils import get_supplier_usages
usages = get_supplier_usages(
supplier="Supplier A", billing_period="01/2026"welche )die sud = frappe.get_doc({
"doctype": "Supplier Usage Data",
"supplier": "Supplier A",
"billing_period": "01/2026",
"month": 1,
"year": 2026
})
sud._completed_usages = usages
sud.save()
Unterschiede
custom_gridware_partygridware_supplier_partyCustomer Usage DetailSupplier Usage Detailgcc.gridware_partygc.supplier9. Item-Mapping und Preisgruppen
Quelle: becharged/becharged/utils.py (Funktion: get_item_code_from_mapping)
Mapping-Logik
def get_item_code_from_mapping(contract_type: str, vat_rate: float) -> Optional[str]:
"""
Mappt Contract Type + VAT zu Item Code
"""
settings = frappe.get_single("Becharged Settings")
# Normalisiere VAT (z.B. 19Basis für 19%)die normalized_vatspätere =Rechnungsstellung flt(vat_rate)
if normalized_vat > 100:
normalized_vat = normalized_vat / 100
for mapping in settings.contract_type_mappings:
if (mapping.contract_type == contract_type and
abs(flt(mapping.vat_rate) - normalized_vat) < 0.01):
return mapping.item_code
return None
Mapping-Beispiel (in Becharged Settings)
Contract Type | VAT % | Item Code
─────────────────────────────────────────
USAGE_POSTPAID | 19 | CHARGING-POSTPAID-19
USAGE_POSTPAID | 0 | CHARGING-POSTPAID-0
AUTHORIZATION | 19 | CHARGING-AUTH-19
USAGE_HUBJECT_CPO_ROAMING | 19 | (wird übersprungen)
Nutzung in Rechnungserstellung
# Gruppiere nach Contract Type + VAT + Item Code
item_groups = {}
for detail in usage_data.usage_details:
# Überspringe bestimmte Typen
if detail.contract_type in ["USAGE_ADHOC", "USAGE_HUBJECT_CPO_ROAMING"]:
continue
group_key = f"{detail.contract_type}|{detail.vat}|{detail.item_code}"
if group_key not in item_groups:
item_groups[group_key] = {
"item_code": detail.item_code,
"contract_type": detail.contract_type,
"vat": detail.vat,
"total_price_excl_vat": 0,
"count": 0
}
item_groups[group_key]["total_price_excl_vat"] += detail.price_excl_vat
item_groups[group_key]["count"] += 1
# Jede Gruppe wird 1 Rechnungszeile
for group in item_groups.values():
invoice.append("items", {
"item_code": group["item_code"],
"qty": 1,
"rate": group["total_price_excl_vat"],
"description": f"Charging sessions for {period} ({group['contract_type']}, {group['vat']}% VAT) - {group['count']} sessions"
})
10. Fehlerfälle & Logging
Gültige Fehler-Szenarien
# 1. Keine Customer/Supplier-Party gefunden
if not customer:
frappe.log_error(
title=f"No Customer found for Gridware Usage {usage.name}",
message=f"Person: {usage.gridware_party_person}, Org: {usage.gridware_party_organization}"
)
continue
# 2. Contract Type Mapping fehlt
if not item_code:
frappe.log_error(
title=f"No item mapping found for {contract_type} + {vat}% VAT",
message=f"Customer: {customer}, Usage: {usage.name}, Period: {billing_period}"
)
continue
# 3. Billing Period kann nicht bestimmt werden
if not billing_period:
frappe.log_error(
title=f"Could not determine billing period for usage {usage.name}",
message=f"End datetime: {usage.end}"
)
continue
# 4. Pending Usages vorhanden
if pending_count > 0:
frappe.throw(_(
f"Found {pending_count} pending Gridware Usage record(s). "
f"Wait for all usages to complete before creating Usage Data."
))
# 5. Duplikat-Aggregation
if usage_already_aggregated:
frappe.throw(
f"The following Gridware Usage records are already aggregated: {usage_ids}"
)
11. Beispiel-Workflow: Von API zu Rechnung
Szenario
bilden.
Kunde "Max Müller" lädt am 15.01.2026 sein Elektroauto
Schritt 1: Gridware Usage erstellt (Pending)
Gridware Usage GW-2026-001
├─ gridware_usage_id: 12345
├─ gridware_contract: Contract-001
├─ start: 2026-01-15 10:00
├─ end: 2026-01-15 11:30
├─ status: Pending
└─ Details: werden async abgerufen
Schritt 2: Background Job holt Details (Complete)
Gridware Usage GW-2026-001
├─ consumption_value_in_kwh: 45.0
├─ usage_price_payment_net_amount: 105.04
├─ usage_price_payment_amount: 125.00
├─ rental_object_display_name: "Charging Point Tesla 1"
├─ user_usage_medium_label: "NFC-7890"
└─ status: Complete ✓
Schritt 3: Benutzer öffnet Customer Usage Data
Customer Usage Data: Max Müller - 01/2026
├─ customer: Max Müller
├─ billing_period: 01/2026
├─ [auto_fetch_completed_usages]
│ └─ Query: get_customer_usages(customer="Max Müller", billing_period="01/2026")
│ └─ Findet: GW-2026-001 (Complete, nicht aggregiert)
├─ [aggregate_completed_usages]
│ └─ Item Mapping: USAGE_POSTPAID + 19% → CHARGING-POSTPAID-19
│ └─ Neue Zeile in usage_details
├─ [calculate_totals]
│ ├─ total_consumption: 45.0 kWh
│ ├─ total_price_excl_vat: 105.04 €
│ ├─ total_price_incl_vat: 125.00 €
│ └─ total_charging_sessions: 1
└─ status: Uninvoiced
Schritt 4: Benutzer erstellt Sales Invoice
Sales Invoice SI-2026-001
├─ customer: Max Müller
├─ custom_customer_usage_data: "Max Müller - 01/2026"
├─ items:
│ └─ CHARGING-POSTPAID-19 | qty=1 | rate=105.04 €
└─ docstatus: 0 (Draft)
↓ [Submit]
└─ docstatus: 1 (Submitted)
Schritt 5: Status-Synchronisation
Customer Usage Data: Max Müller - 01/2026
├─ [on_update von Sales Invoice]
├─ [_set_status_from_invoice]
└─ status: Invoiced ✓
12. Best Practices
✓ DO's
# 1. Immer Status prüfen vor Aggregation
if usage.status != "Complete":
continue
# 2. Duplikate checken
validate_usages_not_aggregated(usage_list)
# 3. Auto-Fetch nutzen statt manuell zu laden
# Usage Data öffnen → auto_fetch_completed_usages wird automatisch aufgerufen
# 4. Fehler loggieren mit vollem Kontext
frappe.log_error(
title="Usage Aggregation Error - GW-001",
message=f"Customer: {customer}, Period: {billing_period}, Error: {str(e)}"
)
# 5. Background Jobs für lange Operationen
frappe.enqueue(
create_customer_usage_data,
filters=filters,
queue="long",
timeout=1200
)
✗ DON'Ts
# 1. Nicht direkt SQL schreiben
# ✗ Schlecht:
frappe.db.sql("SELECT ... WHERE status='Complete'")
# ✓ Gut:
get_customer_usages(customer="...", billing_period="...")
# 2. Nicht ohne Duplikat-Check aggregieren
# ✗ Schlecht:
for usage in usages:
doc.append("usage_details", ...)
# ✓ Gut:
validate_usages_not_aggregated(usage_list)
for usage in usages:
doc.append("usage_details", ...)
# 3. Nicht auf Pending Usages ignorieren
# ✗ Schlecht:
for usage in all_usages: # Auch Pending!
aggregate(usage)
# ✓ Gut:
complete_usages = [u for u in all_usages if u.status == "Complete"]
for usage in complete_usages:
aggregate(usage)
13. Testing-Checkliste
get_customer_usages()Ressourcen
becharged/becharged/doctype/customer_usage_data/customer_usage_data.pybecharged/becharged/doctype/supplier_usage_data/supplier_usage_data.pybecharged/becharged/utils.pybecharged/becharged/doctype/gridware_usage/gridware_usage.py