e-document.io

SAP-Integration

Anleitung zur Anbindung von SAP ECC und S/4HANA an die e-document.io API. IDoc INVOIC02 erzeugen, PDF generieren und beides automatisch an die API senden.

Übersicht

e-document.io empfängt SAP IDoc INVOIC02 und optionale Rechnungs-PDFs über eine REST API. Die Konvertierung in XRechnung (CII, UBL oder ZUGFeRD) erfolgt automatisch. Der Versand läuft über Peppol oder E-Mail — je nach Geschäftspartner-Konfiguration.

1 IDoc erzeugen

Nachrichtensteuerung erzeugt IDoc INVOIC02 bei Faktura-Buchung.

2 PDF erzeugen

SAPscript/Smart Forms/Adobe Forms generiert Rechnungs-PDF.

3 API senden

ABAP-Klasse sendet IDoc-XML + PDF per base64 an die API.

Der gesamte Prozess läuft automatisch: VF01/VF02 (Faktura buchen) → Nachrichtensteuerung → IDoc + PDF erzeugen → API-Aufruf → e-document.io versendet über Peppol oder E-Mail.

Voraussetzungen

Komponente Beschreibung
API-Key Im Dashboard unter API-Keys erstellen. Beginnt mit edoc_
Org-ID Ihre Organisations-ID (sichtbar im Dashboard unter Einstellungen)
Geschäftspartner Partner mit GP-Nummer anlegen, die der SAP-Kundennummer (KUNNR) entspricht
RFC-Destination SM59 → Typ G → Host: e-document.io, Port 443, SSL aktiv, Pfadpräfix: /api/v1
IDoc-Basis Nachrichtenart, Partnervereinbarung und Port für INVOIC02 konfiguriert (WE20/WE21)
Formular SAPscript, Smart Form oder Adobe Form für die Rechnungsausgabe (z.B. RVORDER01)

Architektur

Der gesamte Prozess wird durch die SAP-Nachrichtensteuerung (NAST) ausgelöst:

SAP-Faktura (VF01/VF02)
  ├── Nachrichtensteuerung (NACE/NAST)
  │     │
  │     ├── Nachricht ZE01: IDoc INVOIC02 erzeugen
  │     │     └── IDOC_OUTPUT_INVOIC02 → IDoc-XML
  │     │
  │     └── Nachricht ZE02: PDF erzeugen
  │           └── Smart Form → OTF → PDF
  ├── Z-Programm: ABAP-Klasse ZCL_EDOCUMENT_CLIENT
  │     │
  │     ├── IDoc-XML + PDF → Base64 kodieren
  │     │
  │     └── POST /api/v1/invoices/send
  │           {
  │             "document_type": "idoc",
  │             "xml": "base64...",
  │             "pdf": "base64...",
  │             "recipient": {"partner_number": "KUNNR"}
  │           }
e-document.io
  ├── IDoc → XRechnung (CII/UBL/ZUGFeRD) konvertieren
  ├── Format nach Empfänger-Präferenz wählen
  └── Versand via Peppol oder E-Mail

Nachrichtensteuerung (NACE)

Die Nachrichtensteuerung löst bei jeder Faktura-Buchung automatisch die IDoc- und PDF-Erzeugung aus. Konfiguration über Transaktion NACE, Anwendung V3 (Faktura).

Schritt 1: Nachrichtenart anlegen

Feld Wert Beschreibung
Transaktion NACE Nachrichtensteuerung pflegen
Anwendung V3 Faktura
Nachrichtenart ZE01 E-Rechnung via e-document.io
Versandart 1 (Sofort) Wird bei Faktura-Buchung sofort ausgeführt

Schritt 2: Verarbeitungsroutine zuordnen

In NACE unter der Nachrichtenart ZE01 die Verarbeitungsroutine pflegen:

Feld Wert
Programm (Formular) ZEDOCUMENT_SEND_INVOICE
FORM-Routine ENTRY_EDOCUMENT
Medium 8 (Sonderfunktion)

Schritt 3: Konditionssätze pflegen

In Transaktion VV31 (oder MN04) die Konditionssätze für die Nachrichtenart ZE01 pflegen. Zuordnung pro Verkaufsorganisation/Kunde oder generell für alle Fakturen.

Tipp: Verwenden Sie die Versandart 1 (Sofort), damit die E-Rechnung bei jeder Faktura-Buchung (VF01) automatisch erzeugt und versendet wird. Alternativ: Versandart 4 (Sofort, nicht einzeln) für Batch-Verarbeitung.

IDoc INVOIC02 erzeugen

Das IDoc INVOIC02 enthält alle Rechnungsdaten (Kopf, Positionen, Partner, Steuern, Bankverbindung). Es wird im ABAP-Programm direkt über den Funktionsbaustein erzeugt.

IDoc INVOIC02 aus Faktura erzeugen

*&---------------------------------------------------------------------*
*& IDoc INVOIC02 aus Fakturabeleg erzeugen
*&---------------------------------------------------------------------*
METHOD generate_idoc_xml.
  " iv_vbeln: Fakturanummer
  " rv_xml: IDoc als XML-String (RETURNING)
  DATA: ls_control  TYPE edidc,
        lt_data     TYPE TABLE OF edidd,
        lt_idocs    TYPE TABLE OF edidc,
        lv_docnum   TYPE edi_docnum.
  " Faktura lesen
  SELECT SINGLE * FROM vbrk INTO @DATA(ls_vbrk)
    WHERE vbeln = @iv_vbeln.
  CHECK sy-subrc = 0.
  SELECT * FROM vbrp INTO TABLE @DATA(lt_vbrp)
    WHERE vbeln = @iv_vbeln.
  " IDoc-Kontrollsatz
  ls_control-mestyp = 'INVOIC'.
  ls_control-idoctp = 'INVOIC02'.
  ls_control-rcvprt = 'LS'.
  ls_control-rcvprn = 'EDOCUMENT'.
  ls_control-sndprt = 'LS'.
  ls_control-sndprn = sy-sysid.
  " === Kopfdaten (E1EDK01) ===
  DATA: ls_e1edk01 TYPE e1edk01.
  ls_e1edk01-curcy = ls_vbrk-waerk.
  ls_e1edk01-zterm = ls_vbrk-zterm.
  APPEND VALUE edidd( segnam = 'E1EDK01'
                      sdata  = ls_e1edk01 ) TO lt_data.
  " === Referenzen (E1EDK02) ===
  " Rechnungsnummer (QUALF 012 = Rechnungsnummer)
  APPEND VALUE edidd( segnam = 'E1EDK02' sdata = VALUE e1edk02(
    qualf = '012' belnr = iv_vbeln
    datum = ls_vbrk-fkdat ) ) TO lt_data.
  " Bestellreferenz (QUALF 001 = Bestellnummer)
  IF ls_vbrk-bstnk_vf IS NOT INITIAL.
    APPEND VALUE edidd( segnam = 'E1EDK02' sdata = VALUE e1edk02(
      qualf = '001' belnr = ls_vbrk-bstnk_vf ) ) TO lt_data.
  ENDIF.
  " === Steuer-IDs (E1EDK14) ===
  " USt-IdNr. Verkäufer (QUALF 006)
  APPEND VALUE edidd( segnam = 'E1EDK14' sdata = VALUE e1edk14(
    qualf = '006' orgid = ls_vbrk-stceg ) ) TO lt_data.
  " USt-IdNr. Käufer (QUALF 007) — aus Kundenstamm
  SELECT SINGLE stceg FROM kna1
    INTO @DATA(lv_buyer_vat)
    WHERE kunnr = @ls_vbrk-kunrg.
  APPEND VALUE edidd( segnam = 'E1EDK14' sdata = VALUE e1edk14(
    qualf = '007' orgid = lv_buyer_vat ) ) TO lt_data.
  " === Partner (E1EDKA1) ===
  " Rechnungssteller (RS), Regulierer (RG), Rechnungsempfänger (RE)
  fill_partner( EXPORTING iv_parvw = 'RS' iv_kunnr = ls_vbrk-bukrs CHANGING ct_data = lt_data ).
  fill_partner( EXPORTING iv_parvw = 'RG' iv_kunnr = ls_vbrk-kunrg CHANGING ct_data = lt_data ).
  fill_partner( EXPORTING iv_parvw = 'RE' iv_kunnr = ls_vbrk-kunre CHANGING ct_data = lt_data ).
  " === Positionen (E1EDP01) ===
  LOOP AT lt_vbrp INTO DATA(ls_vbrp).
    DATA: ls_e1edp01 TYPE e1edp01.
    ls_e1edp01-posex = ls_vbrp-posnr.
    ls_e1edp01-menge = ls_vbrp-fkimg.
    ls_e1edp01-menee = ls_vbrp-vrkme.
    APPEND VALUE edidd( segnam = 'E1EDP01'
                        sdata  = ls_e1edp01 ) TO lt_data.
    " Materialreferenz (E1EDP19 — QUALF 002 = EAN, 003 = Käufer-Artikel)
    APPEND VALUE edidd( segnam = 'E1EDP19' sdata = VALUE e1edp19(
      qualf = '002' idtnr = ls_vbrp-matnr ) ) TO lt_data.
    " Preise (E1EDP26 — QUALF 003 = Bruttopreis)
    APPEND VALUE edidd( segnam = 'E1EDP26' sdata = VALUE e1edp26(
      betrg = ls_vbrp-netwr / ls_vbrp-fkimg
      qualf = '003' ) ) TO lt_data.
    " Steuer (E1EDP05 — MWSKZ = Steuerkennzeichen)
    APPEND VALUE edidd( segnam = 'E1EDP05' sdata = VALUE e1edp05(
      mwskz = ls_vbrp-mwskz mwsbt = ls_vbrp-mwsbp ) ) TO lt_data.
  ENDLOOP.
  " === Summen (E1EDS01) ===
  " Nettobetrag
  APPEND VALUE edidd( segnam = 'E1EDS01' sdata = VALUE e1eds01(
    sumid = '010' summe = ls_vbrk-netwr ) ) TO lt_data.
  " Bruttobetrag
  DATA(lv_brutto) = ls_vbrk-netwr + ls_vbrk-mwsbk.
  APPEND VALUE edidd( segnam = 'E1EDS01' sdata = VALUE e1eds01(
    sumid = '011' summe = lv_brutto ) ) TO lt_data.
  " === Bankverbindung (E1EDK17) ===
  SELECT SINGLE bankn, bankl FROM t012k
    INTO @DATA(ls_bank)
    WHERE bukrs = @ls_vbrk-bukrs.
  IF sy-subrc = 0.
    " IBAN aus TIBAN lesen
    SELECT SINGLE iban, swift FROM tiban
      INTO @DATA(ls_iban)
      WHERE banks = ls_vbrk-bukrs+0(2)
        AND bankl = ls_bank-bankl
        AND bankn = ls_bank-bankn.
    APPEND VALUE edidd( segnam = 'E1EDK17' sdata = VALUE e1edk17(
      qualf = '001'
      bankn = ls_iban-iban
      banks = ls_iban-swift ) ) TO lt_data.
  ENDIF.
  " === IDoc als XML serialisieren ===
  CALL FUNCTION 'IDOC_XML_TRANSFORM'
    EXPORTING
      idoc_control = ls_control
    TABLES
      idoc_data    = lt_data
    IMPORTING
      xml_string   = rv_xml.
ENDMETHOD.

Hinweis: QUALF = Qualifier, BELNR = Belegnummer, IDTNR = Identnummer, BETRG = Betrag. e-document.io erwartet die Standard-INVOIC02-Struktur.

IDoc-Segmente Übersicht

Segment Beschreibung SAP-Quelle
E1EDK01Kopfdaten (Währung, Zahlungsbedingung)VBRK
E1EDK02Referenzen (Rechnungsnr., Bestellung, Datum)VBRK
E1EDK14Steuer-IDs (USt-IdNr. Verkäufer/Käufer, GLN)VBRK, KNA1
E1EDKA1Partner (Rechnungssteller RS, Empfänger RE)KNA1, T001
E1EDP01Positionen (Menge, Einheit)VBRP
E1EDP19Materialreferenz pro PositionVBRP-MATNR
E1EDP26Preise pro PositionVBRP-NETWR
E1EDP05Steuer pro PositionVBRP-MWSBP
E1EDS01Summen (Netto, Brutto)VBRK-NETWR
E1EDK17Bankverbindung (IBAN, SWIFT)T012K, TIBAN
E1EDKT1Texte (Positionsbezeichnungen)VBRP-ARKTX

BAdI-Erweiterung (Kundenerweiterung)

Über ein BAdI (Business Add-In) können Kunden die IDoc-Erzeugung an definierten Stellen erweitern — z.B. eigene Segmente hinzufügen, Felder überschreiben oder zusätzliche Datenquellen anbinden. Das BAdI ist mehrstufig aufgebaut: vor der IDoc-Erzeugung, nach jeder Position und nach Abschluss.

Warum BAdI?

BAdI ist der SAP-Standard-Erweiterungsmechanismus ab ECC 6.0. Im Gegensatz zu User-Exits oder Customer-Exits unterstützt BAdI Mehrfach-Implementierungen, Filter und ist upgrade-sicher. In S/4HANA ist BAdI der einzig empfohlene Ansatz.

Schritt 1: BAdI-Interface definieren

Interface ZIF_EDOC_IDOC_ENRICH

Das Interface definiert drei Eingriffspunkte bei der IDoc-Erzeugung:

INTERFACE zif_edoc_idoc_enrich PUBLIC.
  TYPES:
    BEGIN OF ty_context,
      vbeln  TYPE vbeln_vf,    " Fakturanummer
      bukrs  TYPE bukrs,       " Buchungskreis
      kunag  TYPE kunag,       " Auftraggeber
      kunrg  TYPE kunrg,       " Regulierer
      kunre  TYPE kunre,       " Rechnungsempfänger
      vkorg  TYPE vkorg,       " Verkaufsorganisation
      fkart  TYPE fkart,       " Fakturaart
      waerk  TYPE waerk,       " Währung
    END OF ty_context.
  METHODS:
    " Wird VOR der IDoc-Erzeugung aufgerufen.
    " Hier können Kopf-Segmente ergänzt oder Standardwerte
    " überschrieben werden.
    before_create
      IMPORTING
        is_context TYPE ty_context
        is_vbrk    TYPE vbrk
      CHANGING
        ct_data    TYPE edidd_tt,  " IDoc-Segmente
    " Wird NACH jeder Faktura-Position aufgerufen.
    " Hier können positions-spezifische Segmente ergänzt werden
    " (z.B. Chargen, Seriennummern, Zusatztexte).
    after_line_item
      IMPORTING
        is_context TYPE ty_context
        is_vbrp    TYPE vbrp       " aktuelle Position
      CHANGING
        ct_data    TYPE edidd_tt,  " IDoc-Segmente
    " Wird NACH der kompletten IDoc-Erzeugung aufgerufen.
    " Letzte Chance, Segmente zu ändern, hinzuzufügen
    " oder zu entfernen. Auch für Validierung geeignet.
    after_create
      IMPORTING
        is_context TYPE ty_context
      CHANGING
        ct_data    TYPE edidd_tt   " IDoc-Segmente
      RAISING
        zcx_edoc_error.            " Abbruch möglich
ENDINTERFACE.

Schritt 2: BAdI-Definition anlegen

Anlage in Transaktion SE18 (BAdI-Pflege):

FeldWert
BAdI-NameZEDOC_IDOC_ENRICH
InterfaceZIF_EDOC_IDOC_ENRICH
Mehrfach-ImplementierungJa (mehrere Kunden-Erweiterungen gleichzeitig)
Filter-abhängigOptional: nach Verkaufsorganisation (VKORG) oder Fakturaart (FKART)
Fallback-KlasseZCL_EDOC_IDOC_ENRICH_DEFAULT (leere Standardimplementierung)
*&---------------------------------------------------------------------*
*& Fallback-Klasse (leere Standardimplementierung)
*&---------------------------------------------------------------------*
CLASS zcl_edoc_idoc_enrich_default DEFINITION PUBLIC FINAL
  CREATE PUBLIC.
  PUBLIC SECTION.
    INTERFACES zif_edoc_idoc_enrich.
ENDCLASS.
CLASS zcl_edoc_idoc_enrich_default IMPLEMENTATION.
  METHOD zif_edoc_idoc_enrich~before_create.
    " Standard: keine Änderung
  ENDMETHOD.
  METHOD zif_edoc_idoc_enrich~after_line_item.
    " Standard: keine Änderung
  ENDMETHOD.
  METHOD zif_edoc_idoc_enrich~after_create.
    " Standard: keine Änderung
  ENDMETHOD.
ENDCLASS.

Schritt 3: BAdI-Aufruf in generate_idoc_xml einbauen

Die generate_idoc_xml-Methode ruft das BAdI an drei Stellen auf:

METHOD generate_idoc_xml.
  DATA: lt_data TYPE TABLE OF edidd.
  " Faktura + Positionen lesen
  SELECT SINGLE * FROM vbrk INTO @DATA(ls_vbrk)
    WHERE vbeln = @iv_vbeln.
  SELECT * FROM vbrp INTO TABLE @DATA(lt_vbrp)
    WHERE vbeln = @iv_vbeln.
  " Kontext für BAdI aufbauen
  DATA(ls_ctx) = VALUE zif_edoc_idoc_enrich=>ty_context(
    vbeln = iv_vbeln    bukrs = ls_vbrk-bukrs
    kunag = ls_vbrk-kunag  kunrg = ls_vbrk-kunrg
    kunre = ls_vbrk-kunre  vkorg = ls_vbrk-vkorg
    fkart = ls_vbrk-fkart  waerk = ls_vbrk-waerk ).
  " BAdI-Instanz holen
  DATA: lo_badi TYPE REF TO zif_edoc_idoc_enrich.
  GET BADI lo_badi.
  " === Kopfdaten aufbauen (E1EDK01, E1EDK02, E1EDK14, E1EDKA1) ===
  " ... (wie zuvor gezeigt) ...
  " ╔══════════════════════════════════════════════════════╗
  " ║  BAdI-Aufruf 1: BEFORE_CREATE                       ║
  " ║  → Kopf-Segmente ergänzen/überschreiben              ║
  " ╚══════════════════════════════════════════════════════╝
  CALL BADI lo_badi->before_create
    EXPORTING is_context = ls_ctx
              is_vbrk    = ls_vbrk
    CHANGING  ct_data    = lt_data.
  " === Positionen aufbauen (E1EDP01, E1EDP19, E1EDP26, E1EDP05) ===
  LOOP AT lt_vbrp INTO DATA(ls_vbrp).
    " ... Position-Segmente aufbauen (wie zuvor) ...
    " ╔══════════════════════════════════════════════════════╗
    " ║  BAdI-Aufruf 2: AFTER_LINE_ITEM                     ║
    " ║  → pro Position Segmente ergänzen                    ║
    " ║  (z.B. Chargen, Seriennummern, Zusatztexte)          ║
    " ╚══════════════════════════════════════════════════════╝
    CALL BADI lo_badi->after_line_item
      EXPORTING is_context = ls_ctx
                is_vbrp    = ls_vbrp
      CHANGING  ct_data    = lt_data.
  ENDLOOP.
  " === Summen + Bank aufbauen (E1EDS01, E1EDP02) ===
  " ... (wie zuvor gezeigt) ...
  " ╔══════════════════════════════════════════════════════╗
  " ║  BAdI-Aufruf 3: AFTER_CREATE                        ║
  " ║  → letzte Chance: Segmente ändern, validieren        ║
  " ║  → Exception = IDoc wird NICHT gesendet               ║
  " ╚══════════════════════════════════════════════════════╝
  TRY.
      CALL BADI lo_badi->after_create
        EXPORTING is_context = ls_ctx
        CHANGING  ct_data    = lt_data.
    CATCH zcx_edoc_error INTO DATA(lx_err).
      " Abbruch: IDoc wird nicht erzeugt
      CLEAR rv_xml.
      RETURN.
  ENDTRY.
  " === IDoc als XML serialisieren ===
  " ... (wie zuvor mit IDOC_XML_TRANSFORM) ...
ENDMETHOD.

Schritt 4: Kunden-Implementierung (Beispiel)

Der Kunde erstellt eine eigene BAdI-Implementierung in SE19. Hier ein Beispiel, das Chargen-Informationen und eine Projektreferenz hinzufügt:

Beispiel: Kunden-BAdI-Implementierung

*&---------------------------------------------------------------------*
*& Kunden-spezifische BAdI-Implementierung
*& SE19 → Implementierung ZEDOC_IDOC_ENRICH_CUST anlegen
*&---------------------------------------------------------------------*
CLASS zcl_edoc_idoc_enrich_cust DEFINITION PUBLIC FINAL
  CREATE PUBLIC.
  PUBLIC SECTION.
    INTERFACES zif_edoc_idoc_enrich.
ENDCLASS.
CLASS zcl_edoc_idoc_enrich_cust IMPLEMENTATION.
  METHOD zif_edoc_idoc_enrich~before_create.
    " Beispiel: Projektreferenz aus kundenspez. Tabelle ergänzen
    SELECT SINGLE projekt FROM zcust_proj
      INTO @DATA(lv_projekt)
      WHERE vbeln = @is_context-vbeln.
    IF sy-subrc = 0.
      " Eigene Referenz als E1EDK02 mit Qualifier 015
      APPEND VALUE edidd( segnam = 'E1EDK02'
        sdata = VALUE e1edk02(
          qualf = '015'
          belnr = lv_projekt ) ) TO ct_data.
    ENDIF.
    " Beispiel: Verkäufer-Kontakt aus SD-Auftrag ergänzen
    SELECT SINGLE ernam FROM vbak
      INTO @DATA(lv_contact)
      WHERE vbeln = @is_context-vbeln.
    IF sy-subrc = 0.
      " Kontakt als Seller-Contact (E1EDKA1)
      APPEND VALUE edidd( segnam = 'E1EDKA1'
        sdata = VALUE e1edka1(
          parvw   = 'RS'
          bname   = lv_contact ) ) TO ct_data.
    ENDIF.
  ENDMETHOD.
  METHOD zif_edoc_idoc_enrich~after_line_item.
    " Beispiel: Chargen-Info pro Position hinzufügen
    IF is_vbrp-charg IS NOT INITIAL.
      " Charge als Positions-Text (E1EDKT1)
      APPEND VALUE edidd( segnam = 'E1EDKT1'
        sdata = VALUE e1edkt1(
          tdid   = '0002'
          tdline = |Charge: { is_vbrp-charg }| ) ) TO ct_data.
    ENDIF.
    " Beispiel: Kundenartikelnummer ergänzen
    SELECT SINGLE kdmat FROM knmt
      INTO @DATA(lv_kdmat)
      WHERE matnr = @is_vbrp-matnr
        AND kunnr = @is_context-kunag.
    IF sy-subrc = 0.
      " Kunden-Materialnummer als E1EDP19 Qualifier 001
      APPEND VALUE edidd( segnam = 'E1EDP19'
        sdata = VALUE e1edp19(
          qualf = '001'
          idtnr = lv_kdmat ) ) TO ct_data.
    ENDIF.
  ENDMETHOD.
  METHOD zif_edoc_idoc_enrich~after_create.
    " Beispiel: Validierung — IDoc nur senden wenn Netto > 0
    LOOP AT ct_data INTO DATA(ls_seg)
      WHERE segnam = 'E1EDS01'.
      DATA(ls_sum) = CONV e1eds01( ls_seg-sdata ).
      IF ls_sum-sumid = '010' AND ls_sum-summe <= 0.
        " Abbruch: Gutschriften nicht per E-Rechnung senden
        RAISE EXCEPTION TYPE zcx_edoc_error
          EXPORTING textid = zcx_edoc_error=>no_credit_note.
      ENDIF.
    ENDLOOP.
  ENDMETHOD.
ENDCLASS.

Erweiterungspunkte im Überblick

Methode Zeitpunkt Typische Anwendungsfälle
before_create Nach Kopf-Segmenten, vor Positionen Zusätzliche Referenzen (Projektnr., Vertragsnr.), Kontaktdaten, Steuer-Sonderbehandlung, abweichende Bankverbindung
after_line_item Nach jeder Position Chargen-/Seriennummern, Kunden-Artikelnummer, Positionstexte, Rabatt-Details, Lieferschein-Referenz
after_create Nach komplettem IDoc Validierung (z.B. keine Gutschriften), Segmente entfernen/umsortieren, kundenspez. Summen-Segmente, Logging

Hinweis für e-document.io

Alle Segmente, die der Kunde über das BAdI hinzufügt, werden von e-document.io beim Parsen des IDoc INVOIC02 automatisch berücksichtigt. Der IdocParser erkennt die Standard-IDoc-Segmente und ihre Qualifizierer. Kundenspezifische Segmente (Z-Segmente) werden aktuell ignoriert — die Daten sollten daher in Standard-Segmenten mit freien Qualifizierern transportiert werden.

Tipp: Filter-BAdI

Definieren Sie das BAdI mit Filter nach Verkaufsorganisation (VKORG). So können verschiedene Buchungskreise unterschiedliche BAdI-Implementierungen verwenden — z.B. ein Werk ergänzt Chargen, ein anderes ergänzt Projektreferenzen.

PDF-Erzeugung

Das PDF wird über das bestehende Rechnungsformular (SAPscript, Smart Form oder Adobe Form) erzeugt. Der OTF-Spool wird in PDF konvertiert.

PDF aus Faktura-Formular erzeugen

*&---------------------------------------------------------------------*
*& PDF aus SAP-Rechnungsformular erzeugen
*&---------------------------------------------------------------------*
METHOD generate_pdf.
  " iv_vbeln: Fakturanummer
  " rv_pdf: PDF als XSTRING
  DATA: lt_otf       TYPE TABLE OF itcoo,
        lt_lines     TYPE TABLE OF tline,
        lv_bin_size  TYPE i.
  " --- Variante 1: Smart Forms ---
  DATA: lv_fm_name TYPE rs38l_fnam.
  " Smart Form Funktionsbaustein ermitteln
  CALL FUNCTION 'SSF_FUNCTION_MODULE_NAME'
    EXPORTING  formname = 'ZSF_INVOICE'
    IMPORTING  fm_name  = lv_fm_name.
  " Smart Form aufrufen — OTF erzeugen (nicht drucken)
  DATA: ls_control  TYPE ssfctrlop,
        ls_output   TYPE ssfcrescl.
  ls_control-no_dialog = abap_true.
  ls_control-getotf    = abap_true.  " OTF statt Druck
  ls_control-preview   = abap_false.
  CALL FUNCTION lv_fm_name
    EXPORTING
      control_parameters = ls_control
      iv_vbeln           = iv_vbeln
    IMPORTING
      job_output_info    = ls_output.
  lt_otf = ls_output-otfdata.
  " OTF → PDF konvertieren
  CALL FUNCTION 'CONVERT_OTF'
    EXPORTING
      format       = 'PDF'
    IMPORTING
      bin_filesize = lv_bin_size
    TABLES
      otf          = lt_otf
      lines        = lt_lines.
  " Binärtabelle → XSTRING
  CALL FUNCTION 'SCMS_BINARY_TO_XSTRING'
    EXPORTING  input_length = lv_bin_size
    IMPORTING  buffer       = rv_pdf
    TABLES     binary_tab   = lt_lines.
ENDMETHOD.
Das PDF ist optional. Ohne PDF versendet e-document.io die Rechnung als reines XML (CII oder UBL). Mit PDF wird bei ZUGFeRD-Empfängern das XML in das PDF eingebettet (PDF/A-3).

ABAP-Klasse ZCL_EDOCUMENT_CLIENT

Die zentrale Klasse kapselt die gesamte API-Kommunikation. Sie erzeugt IDoc + PDF, kodiert beides in Base64, und sendet es an die e-document.io API.

Klassendefinition

CLASS zcl_edocument_client DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    TYPES:
      BEGIN OF ty_send_result,
        success      TYPE abap_bool,
        document_id  TYPE string,
        status       TYPE string,
        channel      TYPE string,
        http_code    TYPE i,
        error_msg    TYPE string,
      END OF ty_send_result.
    METHODS:
      constructor
        IMPORTING
          iv_api_key TYPE string
          iv_org_id  TYPE string
          iv_rfc_dest TYPE rfcdest DEFAULT 'EDOCUMENT_IO',
      " Kompletter Versand: IDoc + PDF erzeugen und an API senden
      send_invoice
        IMPORTING
          iv_vbeln         TYPE vbeln_vf  " Fakturanummer
          iv_kunnr         TYPE kunnr     " Kundennummer (= GP-Nummer)
          iv_with_pdf      TYPE abap_bool DEFAULT abap_true
        RETURNING
          VALUE(rs_result) TYPE ty_send_result,
      " Status eines gesendeten Dokuments abfragen
      check_status
        IMPORTING
          iv_document_id TYPE string
        RETURNING
          VALUE(rv_status) TYPE string,
      " --- Geschäftspartner-Sync (GP-Export) ---
      " Einzelnen Debitor an e-document.io synchronisieren (Upsert)
      sync_partner
        IMPORTING
          is_partner       TYPE ty_partner_data
        RETURNING
          VALUE(rs_result) TYPE ty_partner_result,
      " Mehrere Debitoren als Batch synchronisieren
      sync_partners
        IMPORTING
          it_partners       TYPE ty_partner_data_t
        RETURNING
          VALUE(rt_results) TYPE ty_partner_result_t.
    TYPES:
      BEGIN OF ty_partner_data,
        partner_number TYPE string,   " KNA1-KUNNR
        name           TYPE string,   " KNA1-NAME1
        company        TYPE string,   " KNA1-NAME2
        email          TYPE string,   " ADR6-SMTP_ADDR
        peppol_id      TYPE string,
        peppol_scheme  TYPE string,
        invoice_format TYPE string,   " cii | ubl | zugferd
        status         TYPE string,   " active | inactive
      END OF ty_partner_data,
      ty_partner_data_t TYPE STANDARD TABLE OF ty_partner_data WITH EMPTY KEY.
    TYPES:
      BEGIN OF ty_partner_result,
        success        TYPE abap_bool,
        partner_id     TYPE string,
        partner_number TYPE string,
        action         TYPE string,   " created | updated
        http_code      TYPE i,
        error_msg      TYPE string,
      END OF ty_partner_result,
      ty_partner_result_t TYPE STANDARD TABLE OF ty_partner_result WITH EMPTY KEY.
  PRIVATE SECTION.
    DATA: mv_api_key  TYPE string,
          mv_org_id   TYPE string,
          mv_rfc_dest TYPE rfcdest.
    METHODS:
      generate_idoc_xml
        IMPORTING iv_vbeln TYPE vbeln_vf
        RETURNING VALUE(rv_xml) TYPE string,
      generate_pdf
        IMPORTING iv_vbeln TYPE vbeln_vf
        RETURNING VALUE(rv_pdf) TYPE xstring,
      call_api
        IMPORTING iv_json TYPE string
        RETURNING VALUE(rs_result) TYPE ty_send_result,
      fill_partner
        IMPORTING iv_parvw TYPE parvw    " Partnerrolle (RS, RG, RE)
                  iv_kunnr TYPE kunnr    " Kundennummer
        CHANGING  ct_data  TYPE edidd_tt.
ENDCLASS.

Implementierung: send_invoice

METHOD send_invoice.
  " 1. IDoc-XML erzeugen
  DATA(lv_idoc_xml) = generate_idoc_xml( iv_vbeln ).
  IF lv_idoc_xml IS INITIAL.
    rs_result-error_msg = 'IDoc-Erzeugung fehlgeschlagen'.
    RETURN.
  ENDIF.
  " 2. XML → Base64
  DATA(lv_xml_xstring) = cl_abap_codepage=>convert_to(
    source = lv_idoc_xml codepage = 'UTF-8' ).
  DATA(lv_xml_b64) = cl_http_utility=>encode_x_base64( lv_xml_xstring ).
  " 3. Optional: PDF erzeugen + Base64
  DATA: lv_pdf_b64 TYPE string.
  IF iv_with_pdf = abap_true.
    DATA(lv_pdf) = generate_pdf( iv_vbeln ).
    IF lv_pdf IS NOT INITIAL.
      lv_pdf_b64 = cl_http_utility=>encode_x_base64( lv_pdf ).
    ENDIF.
  ENDIF.
  " 4. JSON-Payload zusammenbauen
  DATA(lv_filename) = |INVOIC02_{ iv_vbeln }.xml|.
  DATA(lv_partner_nr) = CONV string( iv_kunnr ).
  " JSON manuell aufbauen (oder /ui2/cl_json verwenden)
  DATA(lv_json) = |\{ &&
    |"org_id":"{ mv_org_id }", &&
    |"document_type":"idoc", &&
    |"xml":"{ lv_xml_b64 }", &&
    |"filename":"{ lv_filename }",|.
  IF lv_pdf_b64 IS NOT INITIAL.
    lv_json = lv_json && |"pdf":"{ lv_pdf_b64 }",|.
  ENDIF.
  lv_json = lv_json &&
    |"recipient":\{"partner_number":"{ lv_partner_nr }"\}| &&
    |\}|.
  " 5. API aufrufen
  rs_result = call_api( lv_json ).
ENDMETHOD.

Implementierung: call_api (HTTP-Client)

METHOD call_api.
  DATA: lo_client TYPE REF TO if_http_client.
  " HTTP-Client über RFC-Destination erzeugen
  CALL METHOD cl_http_client=>create_by_destination
    EXPORTING  destination = mv_rfc_dest
    IMPORTING  client      = lo_client.
  " Request konfigurieren
  lo_client->request->set_header_field(
    name = '~request_uri' value = '/api/v1/invoices/send' ).
  lo_client->request->set_header_field(
    name = '~request_method' value = 'POST' ).
  lo_client->request->set_header_field(
    name = 'Content-Type' value = 'application/json' ).
  lo_client->request->set_header_field(
    name = 'X-API-Key' value = mv_api_key ).
  " JSON-Payload setzen
  lo_client->request->set_cdata( data = iv_json ).
  " Senden + Empfangen
  lo_client->send( EXCEPTIONS OTHERS = 1 ).
  IF sy-subrc <> 0.
    rs_result-error_msg = 'HTTP-Verbindungsfehler'.
    RETURN.
  ENDIF.
  lo_client->receive( EXCEPTIONS OTHERS = 1 ).
  IF sy-subrc <> 0.
    rs_result-error_msg = 'HTTP-Empfangsfehler'.
    lo_client->close( ).
    RETURN.
  ENDIF.
  " Response auswerten
  lo_client->response->get_status(
    IMPORTING code = rs_result-http_code ).
  DATA(lv_response) = lo_client->response->get_cdata( ).
  lo_client->close( ).
  IF rs_result-http_code = 202.  " Accepted
    rs_result-success = abap_true.
    " JSON parsen — id und channel extrahieren
    " (Vereinfacht — in Produktion /ui2/cl_json verwenden)
    FIND REGEX '"id"\s*:\s*"([^"]+)"' IN lv_response
      SUBMATCHES rs_result-document_id.
    FIND REGEX '"status"\s*:\s*"([^"]+)"' IN lv_response
      SUBMATCHES rs_result-status.
    FIND REGEX '"channel"\s*:\s*"([^"]+)"' IN lv_response
      SUBMATCHES rs_result-channel.
  ELSE.
    " Fehlermeldung aus Response extrahieren
    FIND REGEX '"error"\s*:\s*"([^"]+)"' IN lv_response
      SUBMATCHES rs_result-error_msg.
    IF rs_result-error_msg IS INITIAL.
      rs_result-error_msg = |HTTP { rs_result-http_code }: { lv_response }|.
    ENDIF.
  ENDIF.
ENDMETHOD.

Implementierung: constructor

METHOD constructor.
  mv_api_key  = iv_api_key.
  mv_org_id   = iv_org_id.
  mv_rfc_dest = iv_rfc_dest.
ENDMETHOD.

Implementierung: check_status

METHOD check_status.
  DATA: lo_client TYPE REF TO if_http_client.
  CALL METHOD cl_http_client=>create_by_destination
    EXPORTING  destination = mv_rfc_dest
    IMPORTING  client      = lo_client.
  DATA(lv_uri) = |/api/v1/ausgang/{ iv_document_id }|.
  lo_client->request->set_header_field(
    name = '~request_uri' value = lv_uri ).
  lo_client->request->set_header_field(
    name = '~request_method' value = 'GET' ).
  lo_client->request->set_header_field(
    name = 'X-API-Key' value = mv_api_key ).
  lo_client->send( EXCEPTIONS OTHERS = 1 ).
  IF sy-subrc <> 0.
    rv_status = 'CONNECTION_ERROR'.
    RETURN.
  ENDIF.
  lo_client->receive( EXCEPTIONS OTHERS = 1 ).
  DATA(lv_response) = lo_client->response->get_cdata( ).
  lo_client->close( ).
  " Status aus JSON-Response extrahieren
  FIND REGEX '"status"\s*:\s*"([^"]+)"' IN lv_response
    SUBMATCHES rv_status.
  IF rv_status IS INITIAL.
    rv_status = 'UNKNOWN'.
  ENDIF.
ENDMETHOD.

Implementierung: fill_partner

METHOD fill_partner.
  " E1EDKA1-Segment für eine Partnerrolle befüllen
  DATA: ls_e1edka1 TYPE e1edka1.
  ls_e1edka1-parvw = iv_parvw.
  IF iv_parvw = 'RS'.  " Rechnungssteller = eigene Firma
    SELECT SINGLE butxt, adrnr FROM t001
      INTO @DATA(ls_t001)
      WHERE bukrs = @iv_kunnr.
    ls_e1edka1-name1 = ls_t001-butxt.
    " Adresse aus ADRC lesen
    SELECT SINGLE street, house_num1, post_code1, city1, country
      FROM adrc INTO @DATA(ls_adr)
      WHERE addrnumber = @ls_t001-adrnr.
    ls_e1edka1-stras = ls_adr-street.
    ls_e1edka1-hausnr = ls_adr-house_num1.
    ls_e1edka1-pstlz = ls_adr-post_code1.
    ls_e1edka1-ort01 = ls_adr-city1.
    ls_e1edka1-land1 = ls_adr-country.
  ELSE.  " RG, RE = Kundenstamm
    SELECT SINGLE name1, name2, stras, pstlz, ort01, land1
      FROM kna1 INTO CORRESPONDING FIELDS OF @ls_e1edka1
      WHERE kunnr = @iv_kunnr.
  ENDIF.
  APPEND VALUE edidd( segnam = 'E1EDKA1'
                      sdata  = ls_e1edka1 ) TO ct_data.
ENDMETHOD.

Aufruf aus der Nachrichtensteuerung

Dieses Programm wird als Verarbeitungsroutine in NACE hinterlegt:

*&---------------------------------------------------------------------*
*& Report ZEDOCUMENT_SEND_INVOICE
*&---------------------------------------------------------------------*
REPORT zedocument_send_invoice.
*&---------------------------------------------------------------------*
*& Entry-Point für Nachrichtensteuerung (NACE)
*&---------------------------------------------------------------------*
FORM entry_edocument USING us_return  TYPE nast_return
                           us_screen  TYPE nast_screen.
  " Nachrichtensatz lesen (globale Variable NAST)
  DATA(lv_vbeln) = CONV vbeln_vf( nast-objky ).  " Fakturanummer
  " Kundennummer aus Faktura lesen
  SELECT SINGLE kunag FROM vbrk INTO @DATA(lv_kunnr)
    WHERE vbeln = @lv_vbeln.
  IF sy-subrc <> 0.
    us_return-retco = 1.
    us_return-text  = |Faktura { lv_vbeln } nicht gefunden|.
    RETURN.
  ENDIF.
  " API-Key und Org-ID aus Customizing (z.B. Z-Tabelle oder TVARVC)
  DATA: lv_api_key TYPE string VALUE 'edoc_IhrApiKeyHier',
        lv_org_id  TYPE string VALUE 'ihre-org-id'.
  " E-Rechnung erzeugen und senden
  DATA(lo_client) = NEW zcl_edocument_client(
    iv_api_key = lv_api_key
    iv_org_id  = lv_org_id ).
  DATA(ls_result) = lo_client->send_invoice(
    iv_vbeln    = lv_vbeln
    iv_kunnr    = lv_kunnr
    iv_with_pdf = abap_true ).
  IF ls_result-success = abap_true.
    us_return-retco = 0.  " Erfolg
    us_return-text = |E-Rechnung gesendet: { ls_result-document_id }|.
    " Optional: Dokument-ID in Z-Tabelle oder VBRK-Zusatzfeld speichern
    " für spätere Status-Abfrage
  ELSE.
    us_return-retco = 1.  " Fehler
    us_return-text = ls_result-error_msg.
    " Nachricht bleibt auf "nicht verarbeitet" — kann erneut gesendet werden
  ENDIF.
ENDFORM.

Wichtig: GP-Nummer = SAP-Kundennummer

Die SAP-Kundennummer (KUNNR) wird als GP-Nummer (partner_number) an e-document.io gesendet. Stellen Sie sicher, dass die Geschäftspartner in e-document.io mit der entsprechenden GP-Nummer angelegt sind. Die ABAP-Klasse nutzt den neuen Endpunkt POST /api/v1/invoices/send — einfacher als multipart/form-data, da nur JSON mit Base64.

Geschäftspartner-Synchronisation

Damit Rechnungen korrekt zugestellt werden, müssen die Geschäftspartner (Debitoren) in e-document.io vorhanden sein. Die SAP-Kundennummer (KUNNR) dient als eindeutiger Schlüssel (partner_number). Die API bietet einen Upsert-Endpunkt, der pro KUNNR automatisch anlegt oder aktualisiert.

API-Endpunkte

Methode Endpunkt Beschreibung
PUT /api/v1/partners/upsert Anlegen oder Aktualisieren anhand partner_number (= KUNNR). Empfohlen für SAP-Sync.
GET /api/v1/partners Alle Geschäftspartner auflisten (paginiert, mit Filtern)
POST /api/v1/partners Neuen Geschäftspartner anlegen
GET /api/v1/partners/:id Einzelnen Partner abrufen
PUT /api/v1/partners/:id Partner aktualisieren
DELETE /api/v1/partners/:id Partner löschen

Felder

Feld Typ SAP-Feld Beschreibung
org_id string Pflicht. Mandanten-ID.
partner_number string (max 10) KNA1-KUNNR Pflicht für Upsert. SAP-Kundennummer als eindeutiger Schlüssel.
name string KNA1-NAME1 Pflicht. Name des Geschäftspartners.
company string KNA1-NAME2 Firmenname / Zusatz.
email string ADR6-SMTP_ADDR E-Mail-Adresse. Fallback-Kanal wenn kein Peppol.
peppol_id string Peppol-Teilnehmer-ID (z.B. DE367927617).
peppol_scheme string Peppol-Schema (z.B. 0204 für Leitweg-ID).
invoice_format string Bevorzugtes Format: cii, ubl oder zugferd (Standard: cii).
status string KNA1-LOEVM active oder inactive (Standard: active).

Die Typen ty_partner_data, ty_partner_result und die Methoden sync_partner / sync_partners sind bereits in der Klassendefinition oben enthalten.

Implementierung: sync_partner

METHOD sync_partner.
  " JSON-Payload für Upsert aufbauen
  DATA(lv_json) = |\{ &&
    |"org_id":"{ mv_org_id }", &&
    |"partner_number":"{ is_partner-partner_number }", &&
    |"name":"{ is_partner-name }", &&
    |"company":"{ is_partner-company }", &&
    |"email":"{ is_partner-email }", &&
    |"status":"{ is_partner-status }", &&
    |"invoice_format":"{ is_partner-invoice_format }", &&
    |"peppol_id":"{ is_partner-peppol_id }", &&
    |"peppol_scheme":"{ is_partner-peppol_scheme }" &&
    |\}|.
  " HTTP-Client über RFC-Destination erzeugen
  DATA: lo_client TYPE REF TO if_http_client.
  CALL METHOD cl_http_client=>create_by_destination
    EXPORTING  destination = mv_rfc_dest
    IMPORTING  client      = lo_client.
  " Request konfigurieren (PUT /api/v1/partners/upsert)
  lo_client->request->set_header_field(
    name = '~request_uri' value = '/api/v1/partners/upsert' ).
  lo_client->request->set_header_field(
    name = '~request_method' value = 'PUT' ).
  lo_client->request->set_header_field(
    name = 'Content-Type' value = 'application/json' ).
  lo_client->request->set_header_field(
    name = 'X-API-Key' value = mv_api_key ).
  lo_client->request->set_cdata( data = lv_json ).
  " Senden + Empfangen
  lo_client->send( EXCEPTIONS OTHERS = 1 ).
  IF sy-subrc <> 0.
    rs_result-error_msg = 'HTTP-Verbindungsfehler'.
    RETURN.
  ENDIF.
  lo_client->receive( EXCEPTIONS OTHERS = 1 ).
  IF sy-subrc <> 0.
    rs_result-error_msg = 'HTTP-Empfangsfehler'.
    lo_client->close( ).
    RETURN.
  ENDIF.
  " Response auswerten
  lo_client->response->get_status(
    IMPORTING code = rs_result-http_code ).
  DATA(lv_response) = lo_client->response->get_cdata( ).
  lo_client->close( ).
  IF rs_result-http_code = 200 OR rs_result-http_code = 201.
    rs_result-success = abap_true.
    rs_result-partner_number = is_partner-partner_number.
    " Partner-ID und Action aus Response parsen
    FIND REGEX '"id"\s*:\s*"([^"]+)"' IN lv_response
      SUBMATCHES rs_result-partner_id.
    FIND REGEX '"action"\s*:\s*"([^"]+)"' IN lv_response
      SUBMATCHES rs_result-action.
  ELSE.
    FIND REGEX '"error"\s*:\s*"([^"]+)"' IN lv_response
      SUBMATCHES rs_result-error_msg.
    IF rs_result-error_msg IS INITIAL.
      rs_result-error_msg = |HTTP { rs_result-http_code }: { lv_response }|.
    ENDIF.
  ENDIF.
ENDMETHOD.

Implementierung: sync_partners (Batch)

METHOD sync_partners.
  " Alle übergebenen Partner einzeln synchronisieren
  LOOP AT it_partners INTO DATA(ls_partner).
    DATA(ls_result) = sync_partner( ls_partner ).
    APPEND ls_result TO rt_results.
    IF ls_result-success = abap_true.
      WRITE: / |✓ { ls_result-partner_number }: { ls_result-action }|.
    ELSE.
      WRITE: / |✗ { ls_result-partner_number }: { ls_result-error_msg }|.
    ENDIF.
  ENDLOOP.
ENDMETHOD.

Report: Debitoren-Sync (SM36 / manuell)

Dieser Report liest alle relevanten Debitoren aus SAP und synchronisiert sie mit e-document.io. Er kann manuell gestartet oder als Hintergrundjob in SM36 eingeplant werden (z.B. täglich).

*&---------------------------------------------------------------------*
*& Report ZEDOCUMENT_SYNC_PARTNERS
*& Debitoren-Stammdaten an e-document.io synchronisieren
*&---------------------------------------------------------------------*
REPORT zedocument_sync_partners.
PARAMETERS:
  p_bukrs TYPE bukrs OBLIGATORY,          " Buchungskreis
  p_vkorg TYPE vkorg,                     " Verkaufsorganisation (optional)
  p_kunnr TYPE RANGE OF kunnr,            " Kundennummern-Selektion
  p_only  TYPE abap_bool AS CHECKBOX,     " Nur aktive Debitoren
  p_test  TYPE abap_bool AS CHECKBOX DEFAULT 'X'. " Testlauf
START-OF-SELECTION.
  " API-Key und Org-ID aus Customizing
  DATA: lv_api_key TYPE string VALUE 'edoc_IhrApiKeyHier',
        lv_org_id  TYPE string VALUE 'ihre-org-id'.
  " Debitoren aus KNA1 + KNB1 lesen
  SELECT k~kunnr, k~name1, k~name2, k~loevm
    FROM kna1 AS k
    INNER JOIN knb1 AS b ON b~kunnr = k~kunnr
    WHERE b~bukrs = @p_bukrs
      AND k~kunnr IN @p_kunnr
    INTO TABLE @DATA(lt_debitoren).
  IF sy-subrc <> 0.
    WRITE: / 'Keine Debitoren gefunden.'.
    RETURN.
  ENDIF.
  WRITE: / |{ lines( lt_debitoren ) } Debitoren gefunden.|.
  ULINE.
  " Partner-Tabelle aufbauen
  DATA: lt_partners TYPE zcl_edocument_client=>ty_partner_data_t.
  LOOP AT lt_debitoren INTO DATA(ls_deb).
    " Optional: Nur aktive
    IF p_only = abap_true AND ls_deb-loevm IS NOT INITIAL.
      CONTINUE.
    ENDIF.
    " E-Mail aus Adresse lesen (ADR6)
    DATA(lv_email) = VALUE string( ).
    SELECT SINGLE smtp_addr FROM adr6
      INTO @lv_email
      WHERE addrnumber = ( SELECT adrnr FROM kna1
                           WHERE kunnr = @ls_deb-kunnr )
        AND flgdefault = 'X'.
    APPEND VALUE #(
      partner_number = CONV string( ls_deb-kunnr )
      name           = ls_deb-name1
      company        = ls_deb-name2
      email          = lv_email
      invoice_format = 'cii'   " Standard: XRechnung CII
      status         = COND #( WHEN ls_deb-loevm IS INITIAL
                               THEN 'active' ELSE 'inactive' )
    ) TO lt_partners.
  ENDLOOP.
  IF p_test = abap_true.
    WRITE: / 'TESTLAUF — keine Daten werden gesendet.'.
    LOOP AT lt_partners INTO DATA(ls_p).
      WRITE: / |  { ls_p-partner_number } — { ls_p-name } ({ ls_p-email })|.
    ENDLOOP.
    RETURN.
  ENDIF.
  " Client erzeugen und Batch-Sync starten
  DATA(lo_client) = NEW zcl_edocument_client(
    iv_api_key = lv_api_key
    iv_org_id  = lv_org_id ).
  DATA(lt_results) = lo_client->sync_partners( lt_partners ).
  " Ergebnis ausgeben
  DATA: lv_ok TYPE i, lv_err TYPE i.
  LOOP AT lt_results INTO DATA(ls_res).
    IF ls_res-success = abap_true.
      lv_ok = lv_ok + 1.
    ELSE.
      lv_err = lv_err + 1.
    ENDIF.
  ENDLOOP.
  ULINE.
  WRITE: / |Ergebnis: { lv_ok } erfolgreich, { lv_err } Fehler.|.

Empfohlener Ablauf

1. Einmalig: Report ZEDOCUMENT_SYNC_PARTNERS mit Testlauf starten, um alle Debitoren zu prüfen. 2. Produktivlauf starten, um alle GP anzulegen. 3. SM36-Job einplanen (z.B. täglich nachts), um Änderungen automatisch zu synchronisieren. 4. Danach werden Rechnungen automatisch dem richtigen GP zugeordnet — über die SAP-Kundennummer (KUNNR = partner_number).

Peppol-ID im Kundenstamm

Falls Ihre Kunden eine Peppol-ID haben (z.B. Leitweg-ID für Behörden), können Sie diese in einem Z-Feld im Kundenstamm (KNA1 Append) oder einer eigenen Z-Tabelle pflegen und beim Sync mitgeben. e-document.io prüft automatisch, ob der Empfänger per Peppol erreichbar ist.

Rechnungsempfang (Eingang)

SAP ruft regelmäßig die API ab (Polling), um neue eingehende Rechnungen zu verarbeiten. Empfohlen: Hintergrundjob alle 5 Minuten (SM36).

e-document.io (Empfang via Peppol / E-Mail / API)
  ├── Automatische Validierung + Konvertierung
SAP (Hintergrundjob alle 5 Min.)
  ├── GET /api/v1/erfolgreich  (neue Belege auflisten)
  ├── GET /api/v1/erfolgreich/:id/download?type=xml
  ├── XML parsen → BAPI_INCOMINGINVOICE_CREATE
  └── POST /api/v1/erfolgreich/:id/abholen
XRechnung-Feld (UBL) SAP-Feld Beschreibung
cbc:IDXBLNRExterne Belegnummer
cbc:IssueDateBLDATBelegdatum
cbc:DueDateZFBDTFälligkeitsdatum
cac:AccountingSupplierPartyLIFNRLieferantennummer
cbc:PayableAmountWRBTRRechnungsbetrag
cbc:DocumentCurrencyCodeWAERSWährung
cbc:TaxAmountWMWSTSteuerbetrag

SAP-BAPIs: BAPI_INCOMINGINVOICE_CREATE (MM-Rechnungsprüfung) oder BAPI_ACC_DOCUMENT_POST (FI-Beleg)

Fehlerbehandlung

CodeBedeutungSAP-Reaktion
202 Angenommen (Queued) Dokument-ID speichern, Nachricht als verarbeitet markieren
400 Ungültige Anfrage Parameter prüfen (org_id, xml, recipient fehlt?)
401 Nicht authentifiziert API-Key in TVARVC prüfen
404 Partner nicht gefunden GP-Nummer in e-document.io anlegen (KUNNR → partner_number)
422 IDoc-Konvertierung fehlgeschlagen IDoc-Segmente prüfen (Pflichtfelder vorhanden?)
502 Serverfehler Retry nach 60 Sekunden (max. 3 Versuche)

Nachrichtensteuerung bei Fehler

Wenn ENTRY_EDOCUMENT den Returncode 1 setzt, bleibt die Nachricht auf Status 'nicht verarbeitet'. Sie kann über VF31 (Nachrichtenausgabe wiederholen) oder RSNAST00 erneut gesendet werden.

Middleware / iPaaS

Alternativ zur direkten ABAP-Programmierung können Sie eine Middleware einsetzen:

SAP BTP Integration Suite

iFlow-Designer, native SAP-Konnektoren, IDoc-Trigger. Offiziell von SAP.

Microsoft Power Automate

Low-Code mit SAP-Konnektor. Gut für Microsoft-basierte Umgebungen.

Make (Integromat)

Visueller Workflow-Builder. Schnell aufgesetzt, kostengünstig.

n8n

Open-Source, self-hosted. REST-API-Nodes, Webhook-Trigger.

Middleware-Flow mit neuem API-Endpunkt

Trigger: SAP IDoc-Ausgang (WE20 → Middleware als Partner)
Middleware empfängt IDoc INVOIC02 als XML
Optional: PDF vom SAP-Spool abrufen
Base64-kodieren und JSON zusammenbauen
POST /api/v1/invoices/send
Response-ID speichern für Status-Tracking
Vorteil des neuen Endpunkts: Kein multipart/form-data nötig — einfaches JSON mit Base64. Das vereinfacht die Middleware-Integration erheblich.