Verwendungszweck-Parser mit regulären Ausdrücken

Alexander Tcherniak, Freitag, 7. Februar 2014, 0 Kommentare, Tags: FS-CD | RegEx

Zur Verarbeitung von Zahlungseingängen und zur korrekten Zuordnung zu offenen Posten, Versicherungsobjekten, Vertragskonten bzw. Geschäftspartnern ist es erforderlich, die von der Bank per Kontoauszug übermittelten Verwendungszwecke zu parsen. FS-CD bietet hier standardmäßig nur die in SAP übliche Mustererkennung mit den Wildcards + (genau ein beliebiges Zeichen) oder * (beliebig viele Zeichen) an. In den meisten Fällen wird dies sicherlich ausreichen, insbesondere wenn Einzahlern standardisierte Verwendungszwecke vorgegeben werden können. Es kann jedoch Szenarios geben, in denen die Verwendungszwecke nicht standardisiert werden können, z. B. im B2B-Bereich, wenn von den Einzahlern verschiedene Abrechnungssysteme mit unterschiedlich aufgebauten Verwendungszwecken eingesetzt werden. In einem solchen Szenario stößt man schnell an die Grenzen des SAP-Standards, und eine hartcodierte ABAP-Lösung ist von vorneherein zum Scheitern verurteilt, da sie viel zu unflexibel ist. In einem solchen Szenario bieten sich reguläre Ausdrücke zur Mustererkennung an. Eine Einführung in reguläre Ausdrücke bietet der verlinkte Wikipedia-Artikel.

Lösungsansatz

Der Lösungsansatz, den ich hier beschreiben werde, basiert darauf, dass die Struktur des Verwendungszwecks (welche Elemente sind in welcher Reihenfolge enthalten), mit einem regulären Ausdruck erkannt wird. In einem zweiten Schritt werden diese Elemente dann mit Hilfe der Submatches aus dem Verwendungszweck extrahiert.

Beispiele für Verwendungszwecke:
0212647563/274635
KONTO 021.1426342 RE 685634

Die Kontonummern haben für dieses Beispiel stets den Präfix 021, gefolgt von sieben Stellen. Die Rechnungsnummern sind sechsstellig. Ein regulärer Ausdruck könnte so aussehen:

(021\.?)(\d{7})(\D*)(\d{6})

Bedeutet: Ein Treffer beginnt mit 021, optional gefolgt von einem Punkt. Danach eine siebenstellige Nummer. Als nächstes ein beliebig langer Abschnitt (auch leer), der keine Ziffern enthalten darf. Danach eine sechsstellige Nummer. Die vier in Klammern gesetzten Blöcke bewirken, dass bei einem Treffer diese vier Blöcke als Submatches zurückgegeben werden (ABAP-Befehl find first occurrence of regex … in … results …). Aus diesen vier Submatches lassen sich nun die Selektionswerte für die Zahlstapelposition bilden:

Beispiele:
Versicherungsobjekt-ID (Selektionstyp V): ABC-(2)
Referenzbelegnummer (Selektionstyp X): (4)

Die in Klammern gesetzten Ziffern werden dabei durch das jeweilige Submatch ersetzt und die Ergebnisse in die Zahlstapelposition geschrieben. Das ganze passiert im Zeitpunkt 0950 (Zahlungsstapelübernahme: Selektion ergänzen).

Für die beiden o. g. Verwendungszwecke ergeben sich damit folgende Selektionswerte:
V = ABC-2647563, X = 274635
V = ABC-1426342, X = 685634

Welche Elemente werden für die Umsetzung benötigt? Offensichtlich einen Funktionsbaustein für den Zeitpunkt 0950, und weiterhin eine Customizing-Tabelle, in der die Regeln abgelegt werden. Um den Funktionsbaustein nicht zu umfangreich werden zu lassen, lagere ich den eigentlichen Parser in eine separate Klasse aus.

Die Customizing-Tabelle ZCDRULES

Der Schlüssel dieser Tabelle wird neben dem Mandant die Regel-ID. Jede Regel bekommt somit einen eindeutigen Schlüssel, der beliebig vergeben werden kann. Weiterhin bekommt jede Regel eine Priorität, die die Reihenfolge der Ausführung festlegt.

Da wir von FS-CD im Zeitpunkt 0950 auch den Buchungskreis und das Bankverrechnungskonto übergeben bekommen, bietet es sich für Spezialfälle an, eine Möglichkeit vorzusehen, um die Ausführung bestimmter Regeln auf einzelne Buchungskreise bzw. Bankverrechnungskonten einzuschränken.

Weitere Attribute der Tabelle sind der reguläre Ausdruck, der das Muster des Verwendungszwecks beschreibt, sowie für jeden zu findenden Selektionstyp einen Ausdruck, der beschreibt, wie der Wert aus den Submatches zusammengesetzt wird.

Zusätzlich bekommt die Tabelle noch drei Flags, „Akonto buchen“, „Klärfall erzeugen“ und „Aktiv“. Die ersten beiden Flags werden direkt in die Zahlstapelposition geschrieben, wenn sie gesetzt sind, und erzeugen somit sofort eine Akontobuchung oder einen Klärfall, ohne dass die Position zuvor in die Verrechnungssteuerung geschickt wird. Mit dem dritten Flag kann eine Regel einfach deaktiviert werden, ohne die Regel gleich löschen zu müssen.

Zur Dokumentation der Regel gibt es noch ein Attribut, in dem Anmerkungen erfasst werden können.

Hier der komplette Aufbau der Tabelle:

Attribut Key Datentyp Beschreibung
CLIENT X CLNT Mandant
RULE_ID X CHAR 10 Regel-ID
PATTERN CHAR 200 Muster für Verwendungszweck (RegExp)
PRIORITY NUMC 3 Priorität der Regel
COMPANY_CODE CHAR 4 Buchungskreis (auch * möglich)
BANK_ACCOUNT CHAR 10 Bankverrechnungskonto (auch * möglich)
INSOBJ_ID CHAR 30 Muster für Versicherungsobjekt (mit (n)-Parametern)
INVOICE_ID CHAR 30 Muster für Referenzbelegnummer (mit (n)-Parametern)
ON_ACCOUNT CHAR 1 Akonto-Flag setzen
CLEARING CHAR 1 Klärfall-Flag setzen
ACTIVE CHAR 1 Kennzeichen, ob die Regel aktiv ist
REMARK CHAR 50 Anmerkung

Die Parser-Klasse ZCL_CD_PARSER

Die Klasse besteht im Wesentlichen aus der Methode PARSE, die für einen übergebenen Verwendungszweck die anzuwendende Regel bestimmt und die gewünschten Elemente zurückliefert. Dazu verwendet die Methode die lokale Hilfsklasse CL_RULE, die eine einzelne Regel repräsentiert. Die Parser-Klasse erzeugt in ihrem Konstruktor für jede Regel ein Objekt von CL_RULE und übergibt ihm den zugehörigen Eintrag aus ZCDRULES.

Die Hilfsklasse hat neben dem Konstruktor die beiden funktionalen Methode APPLIES_TO und GET_RESULT.

Die Methode APPLIES_TO prüft zunächst, ob die Regel angewendet werden darf (Buchungskreis und Bankverrechnungskonto) und wertet dann den regulären Ausdruck aus. Falls erfolgreich, sichert sie noch die Submatches und gibt ABAP_TRUE zurück, ansonsten ABAP_FALSE.

Die Methode GET_RESULT legt eine Kopie des Eintrags aus ZCDRULES an und ersetzt die (n)-Parameter mit den konkreten Submatches.

Hier zunächst der vollständige Quelltext der Hilfsklasse CL_RULE:

*"* use this source file for the definition and implementation of
*"* local helper classes, interface definitions and type
*"* declarations
*----------------------------------------------------------------------*
*       CLASS CL_RULE DEFINITION
*----------------------------------------------------------------------*
*
*----------------------------------------------------------------------*
class CL_RULE definition final.
  public section.
    methods CONSTRUCTOR
      importing
        RULE_DATA type ZCDRULES.
    methods APPLIES_TO
      importing
        COMPANY_CODE type ZCDRULES-COMPANY_CODE
        BANK_ACCOUNT type ZCDRULES-BANK_ACCOUNT
        REFERENCE type CLIKE
      returning
        VALUE(RESULT) type ABAP_BOOL.
    methods GET_RESULT
      returning
        VALUE(RESULT) type ref to ZCDRULES.
  private section.
    data:
      RULE_DATA  type          ZCDRULES,
      SUBMATCHES type table of STRING.
endclass.                    "CL_RULE DEFINITION
*----------------------------------------------------------------------*
*       CLASS CL_RULE IMPLEMENTATION
*----------------------------------------------------------------------*
*
*----------------------------------------------------------------------*
class CL_RULE implementation.
  method CONSTRUCTOR.
    ME->RULE_DATA = RULE_DATA.
  endmethod.                    "CONSTRUCTOR
  method APPLIES_TO.
    data: MATCH_RESULT    type        MATCH_RESULT,
          SUBMATCH_RESULT type ref to SUBMATCH_RESULT,
          SUBMATCH        type        STRING.
    clear SUBMATCHES.
    if RULE_DATA-COMPANY_CODE <> '*' and RULE_DATA-COMPANY_CODE <> COMPANY_CODE.
      return.
    endif.
    if RULE_DATA-BANK_ACCOUNT <> '*' and RULE_DATA-BANK_ACCOUNT <> BANK_ACCOUNT.
      return.
    endif.
    find first occurrence of regex RULE_DATA-PATTERN
         in REFERENCE
         results MATCH_RESULT.
    if SY-SUBRC <> 0.
      return.
    endif.
    loop at MATCH_RESULT-SUBMATCHES reference into SUBMATCH_RESULT.
      if SUBMATCH_RESULT->LENGTH > 0.
        SUBMATCH = REFERENCE+SUBMATCH_RESULT->OFFSET(SUBMATCH_RESULT->LENGTH).
        insert SUBMATCH into table SUBMATCHES.
      else.
        insert initial line into table SUBMATCHES.
      endif.
    endloop.
    RESULT = ABAP_TRUE.
  endmethod.                    "APPLIES_TO
  method GET_RESULT.
    data SUBMATCH type ref to STRING.
    create data RESULT.
    RESULT->* = RULE_DATA.
    loop at SUBMATCHES reference into SUBMATCH.
      replace all occurrences of |({ SY-TABIX })| in RESULT->INSOBJ_ID    with SUBMATCH->*.
      replace all occurrences of |({ SY-TABIX })| in RESULT->INVOICE_ID   with SUBMATCH->*.
    endloop.
  endmethod.                    "GET_RESULT
endclass.                    "CL_RULE IMPLEMENTATION

In der Methode PARSE der Parser-Klasse ZCL_CD_PARSER passiert dann eigentlich nicht mehr viel. Die Methode führt eine Schleife über alle Regel-Objekte durch und ruft für jedes Regel-Objekt die Methode APPLIES_TO auf. Falls diese ABAP_TRUE zurückliefert, ruft die Methode PARSE die Methode GET_RESULT auf und gibt das Ergebnis (die angewendete Regel mit den aufgelösten Elementen) an den Aufrufer zurück. Falls keine Regel angewendet werden konnte, gibt die Methode eine leere Regel mit gesetztem Klärfall-Flag zurück. Wichtig ist noch, dass der Konstruktur der Parser-Klasse die Regeln nach Priorität und Regel-ID vorsortiert, damit später die Regeln in der richtigen und immer gleichen Reihenfolge durchlaufen werden.

Hier nun der vollständige Quelltext der Parser-Klasse:

class ZCL_CD_PARSER definition 
  public
  final .
public section.
*"* public components of class ZCL_CD_PARSER
*"* do not include other source files here!!!
  types:
    RULES_TAB type table of ZCDRULES with default key .
  methods CONSTRUCTOR .
  methods PARSE
    importing
      !COMPANY_CODE type ZCDRULES-COMPANY_CODE
      !BANK_ACCOUNT type ZCDRULES-BANK_ACCOUNT
      !REFERENCE type CLIKE
    returning
      value(RESULT) type ref to ZCDRULES
    raising
      CX_SY_REGEX .
  protected section.
*"* protected components of class ZCL_CD_PARSER
*"* do not include other source files here!!!
private section.
*"* private components of class ZCL_CD_PARSER
*"* do not include other source files here!!!
  data:
    RULES type table of ref to CL_RULE
            with non-unique default key .
ENDCLASS.
CLASS ZCL_CD_PARSER IMPLEMENTATION.
* <signature>---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_CD_PARSER->CONSTRUCTOR
* +-------------------------------------------------------------------------------------------------+
* +--------------------------------------------------------------------------------------</signature>
  method CONSTRUCTOR.
    data: RULE_DATA_TAB type table of ZCDRULES,
          RULE_DATA     type ref to   ZCDRULES,
          RULE          type ref to   CL_RULE.
    select *
      from ZCDRULES
      into table RULE_DATA_TAB
      where ACTIVE = ABAP_TRUE.
    sort RULE_DATA_TAB by PRIORITY RULE_ID.
    loop at RULE_DATA_TAB reference into RULE_DATA .
      create object RULE
        exporting
          RULE_DATA = RULE_DATA->*.
      insert RULE into table RULES.
    endloop.
  endmethod.                    "CONSTRUCTOR
* <signature>---------------------------------------------------------------------------------------+
* | Instance Public Method ZCL_CD_PARSER->PARSE
* +-------------------------------------------------------------------------------------------------+
* | [--->] COMPANY_CODE                   TYPE        ZCDRULES-COMPANY_CODE
* | [--->] BANK_ACCOUNT                   TYPE        ZCDRULES-BANK_ACCOUNT
* | [--->] REFERENCE                      TYPE        CLIKE
* | [<-()] RESULT                         TYPE REF TO ZCDRULES
* | [!CX!] CX_SY_REGEX
* +--------------------------------------------------------------------------------------</SIGNATURE>
  method PARSE.
    data RULE type ref to CL_RULE.
    loop at RULES into RULE.
      check RULE->APPLIES_TO( COMPANY_CODE = COMPANY_CODE
                              BANK_ACCOUNT = BANK_ACCOUNT
                              REFERENCE    = REFERENCE ) = ABAP_TRUE.
      RESULT = RULE->GET_RESULT( ).
*** hier ggf. weitere Prüfungen einbauen ***
      return.
    endloop.
    create data RESULT.
    RESULT->CLEARING = ABAP_TRUE.
  endmethod.                    "PARSE
ENDCLASS.

Der Funktionsbaustein Z_CD_EVENT_0950

Dieser Funktionsbaustein, der in der Transaktion FQEVENTS im Zeitpunkt 0950 eingetragen wird, stellt das Bindeglied zwischen FS-CD und dem Parser dar. Ein wichtiges Detail ist noch, dass der Funktionsbaustein die Regel-ID der angewendeten Regel im Attribut INFOF in der Zahlstapelposition einträgt. Dies hilft ungemein bei der Analyse von Klärfällen, da sofort erkannt werden kann, ob und welche Regel angewendet wurde. Hier eine mögliche Implementierung des Funktionsbausteins:

function Z_CD_EVENT_0950.
*"----------------------------------------------------------------------
*"*"Lokale Schnittstelle:
*"  IMPORTING
*"     REFERENCE(I_FKKZK) LIKE  DFKKZK STRUCTURE  DFKKZK
*"  TABLES
*"      T_FKKZP STRUCTURE  DFKKZP
*"      T_FKKZS STRUCTURE  DFKKZS
*"      T_FKKZV STRUCTURE  DFKKZV OPTIONAL
*"----------------------------------------------------------------------
  data: PARSER    type ref to ZCL_CD_PARSER,
        POSITION  type ref to DFKKZP,
        REFERENCE type        STRING,
        EXT_REF   type ref to DFKKZV,
        RESULT    type ref to ZCDRULES.
  create object PARSER.
  loop at T_FKKZP reference into POSITION.
    REFERENCE = TO_UPPER( POSITION->TXTVW ).
    loop at T_FKKZV reference into EXT_REF where KEYZ1 = POSITION->KEYZ1 and UPOSV = POSITION->UPOSV. "#EC CI_NESTED
      REFERENCE = REFERENCE && TO_UPPER( EXT_REF->TXTVW ).
    endloop.
    RESULT = PARSER->PARSE(
        COMPANY_CODE = POSITION->BUKRS
        BANK_ACCOUNT = POSITION->BVRKO
        REFERENCE    = REFERENCE ).
    if not RESULT->INSOBJ_ID is initial.
      POSITION->SELW2 = RESULT->INSOBJ_ID.
      POSITION->SELT2 = 'V'.
    endif.
    if not RESULT->INVOICE_ID is initial.
      POSITION->SELW3 = RESULT->INVOICE_ID.
      POSITION->SELT3 = 'X'.
    endif.
    POSITION->XAKON = RESULT->ON_ACCOUNT.
    POSITION->XKLAE = RESULT->CLEARING.
    POSITION->INFOF = RESULT->RULE_ID.
  endloop.
endfunction.

Zusammenfassung

Der hier vorgestellte Lösungsansatz stellt ein Grundgerüst dar, das für die unterschiedlichsten Anforderungen erweiterbar ist. Sofern in den im Verwendungszweck übermittelten Nummern Prüfziffern enthalten sind, könnten diese überprüft werden, und falls die Prüfziffer ungültig ist, gleich das Klärfall-Flag gesetzt werden (siehe markierte Stelle in der Methode PARSE).

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden /  Ändern )

Google Foto

Du kommentierst mit Deinem Google-Konto. Abmelden /  Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden /  Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden /  Ändern )

Verbinde mit %s