Skip to main content

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:

    und
    deren Aggregation zu

    Customer Usage Data

    und

    Supplier Usage Data

     für die Abrechnung.

    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 becharged/becharged/doctype/gridware_usage/gridware_usage.pywerden sämtliche von Gridware berechneten Kosten übernommen.

        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:

            (Background
            Job) 2.

            Gridware CompleteContract

            └─ Alle Details

            Contract verfügbarNumber

            (Kosten, Verbrauch, Zeitstempel)

            Contract └─Type

            Bereit

            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.

            Detail-Abruf

            Der └─Vertrag Wirdbestimmt täglichunter automatischanderem:

            erneut
            versucht

            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.

              FeldGridware Usage BedeutungCustomer / Supplier Usage Data
              gridware_usage_idEinzelner Ladevorgang EindeutigeZusammenfassung IDmehrerer aus Gridware APILadevorgänge
              gridware_contractTechnisches Abbild aus Gridware VerknüpfterKaufmännische VertragAggregation innerhalb ERPNext
              start,Wird enddirekt aus Gridware übernommen ZeitraumWird desinnerhalb LadevorgangsERPNext erzeugt
              consumption_value_in_kwhKeine Rechnungsstellung StromverbrauchGrundlage infür kWh
              die usage_price_payment_net_amount Nettobetrag (ohne MwSt.) usage_price_payment_amount Bruttobetrag (mit MwSt.) rental_object_display_name Ladesäulen-Name user_usage_medium_label Nutzer-ID (z.B. NFC-Card)Rechnungsstellung

              Kostenaufschlü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 CustomerGridware Usage Dataden oderunverä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

                Status-Filter: Nur Complete oder Pending Kunde: Über custom_gridware_party auf Customer verknüpft Periode: end Zeitstempel liegt im Abrechnungsmonat Duplikat-Check: Keine bereits in Customer Usage Detail aggr eg rierten Usages Ausschluss-Liste: Weitere anzugebende IDs ausschließen

                5. Aggregation - Vom Usage zur Usage Data

                Quelle: becharged/becharged/utils.py (Klasse: BaseUsageData)

                Was ist Aggregation?

                Eine Aggregation ist das HinzufügenUrsprung eines GridwareLadevorgangs Usagedarstellt, zu 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:

                  Speichern der Usage Data Update/Submit/Cancel des Invoices

                  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

                  Aspekt Customer Supplier Abfrage Über custom_gridware_party auf Customer Über gridware_supplier_party auf Contract Invoice Sales Invoice (Verkauf) Purchase Invoice (Einkauf) Duplikat-Check Customer Usage Detail Supplier Usage Detail Filter-Feld gcc.gridware_party gc.supplier Nutzer-ID Ladendes Elektrofahrzeug CPO (Charging Point Operator)

                  9. 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

                     Gridware Usage mit Status "Complete" vorhanden  get_customer_usages() findet korrekte Usages für Kunde + Periode  Auto-Fetch wird ausgelöst beim Öffnen von Customer Usage Data  Aggregation fügt Usage zu usage_details hinzu  Duplikat-Check blockiert bereits aggregierte Usages  Item-Mapping findet korrekten Item Code  Totals berechnet sich korrekt  Status wechselt zu "Draft Invoice" nach Sales Invoice-Erstellung  Status wechselt zu "Invoiced" nach Submit  Kostenaufschlüsselung (Fix, Energy, Time, Parking) addiert sich korrekt  Pending Usages blockieren Aggregation mit Fehlermeldung

                    Ressourcen

                      Customer Usage Data: becharged/becharged/doctype/customer_usage_data/customer_usage_data.py Supplier Usage Data: becharged/becharged/doctype/supplier_usage_data/supplier_usage_data.py Utils: becharged/becharged/utils.py Gridware Usage: becharged/becharged/doctype/gridware_usage/gridware_usage.py