Global Changecompany – Startsession

Ein Wunsch, so alt wie Navision: Ein pauschaler Mandantenwechsel per Programmcode. Und leider bis heute ein Wunsch geblieben… oder doch nicht?

Changecompany – Problem mit Triggern

Eine typische Aufgabenstellung in Navision ist z.B. das Einlesen von Daten aus einer einzigen Datei in verschiedene Mandanten. Z.B. werden von einem LVS (Lagerverwaltungssystem) Bestände eingelesen, welche im ERP verschiedenen Mandanten zuzuordnen sind.

Auch, wenn Sie eine Großrechneranwendung der 70er/80er ablösen wollen, z.B. Trampolin oder Quattro Pro auf der Siemens Nixdorf, dann kennen Sie/Ihre Anwendung den Begriff „Mandanten“ vielleicht gar nicht.
Grundsätzlich ist genau diese Einlesung sehr einfach. Großrechnerablösung eher nicht so… Die aufnehmende Tabelle wird entweder als DataPerCompany = NO eingestellt, und so können alle Datensätze direkt in einem Rutsch eingelesen werden,
oder die aufnehmende Tabelle wird auf dem Standard DataPerCompany = <YES> gelassen, und pro einzulesenden Datensatz wird mit ImportTable.changecompany(ZielMandant des einzulesenden Datensatzes) der Mandant umgeschaltet.

Beides ist technisch keine Herausforderung.

Schwierig wird es hingegen, a) diese Daten dann korrekt in die ArtikelBuchBlattzeilen des jeweiligen Zielmandanten zu bringen und b) diese Tabelle dann korrekt zu verbuchen.

Warum? Jeder, der mit ChangeCompany gearbeitet hat, ist schon darüber gestolpert.

Wir machen das mal an diesem einfachen Beispiel:

CLEAR(item); //Wichtig: muss VOR dem ChangeCompany stehen! Clear setzt ChangeCompany zurück, im Gegensatz zu Init.
item.CHANGECOMPANY(Mandant2)
Item.INSERT(true); //Das TRUE ist das böse!

Vermutlich wird dieser Code genau so auch funktionieren.
Aber was passiert?
Der Artikel wird in dem Mandanten2 angelegt, jedoch mit einer Artikelnummer aus dem aktuellen Mandanten!
Warum? Sehen wir uns den InsertTrigger der ItemTable 27 mal in dem Gesamtkontext an:

CLEAR(item); 
item.CHANGECOMPANY(Mandant2)
Item.INSERT(true); ->
  IF "No." = '' THEN BEGIN
    GetInvtSetup;
    InvtSetup.TESTFIELD("Item Nos."); //InvtSetup kommt aus Mandant1!
    NoSeriesMgt.InitSeries(InvtSetup."Item Nos.",xRec."No. Series",0D,"No.","No. Series"); //Die Nummernseriencodeunit läuft mit allen Records darin in Mandant 1!
    "Costing Method" := InvtSetup."Default Costing Method"; //InvtSetup kommt aus Mandant1!
  END;

Es gibt keine Möglichkeit, dieses Verhalten zu ändern. Jeder, der das erste Mal mit ChangeCompany arbeitet, stolpert nahezu zwangsläufig über dieses Verhalten. Die normale Erwartung ist (meiner Meinung nach), dass die Tabelle „Weiß“, das Sie in Mandanten2 arbeitet, und daher auch alle Programmlogik in Ihr automatisch und implizit mit Datensätzen aus dem Mandanten2 arbeitet.
Mhm… Spoileralarm: Tut Sie nicht. Der aktive, globale Mandant gilt für die Sitzung/Session, nicht für das Objekt. Also für den aktuellen Userkontext.

Ein weiteres lustiges Beispiel ist dieses:

Customer.get(‚Kundennummer“);
Customer.CHANGECOMPANY(Mandant2);
Customer.calcfields(Balance);

WENN der Kunde mit der „Kundennummer“ in dem Mandanten 2 existiert, so wird calcfields(Balance) den Saldo für… diese Kundennummer in dem aktuellen Mandanten 1 ermitteln! Egal, welcher Kunde das ist! Denn auch die dafür nötigen Detailed Cust. Ledg. Entry (Tabelle 339) werden aus dem Mandanten des Userkontexts, also aus dem aktiven Mandanten gezogen! Gibt es diese Kundennummer (besser: die Detailed Cust. Ledg. Entry (Tabelle 339) Einträge) für diesen Kunden nicht im aktuellen Mandanten, so ist das Ergebnis schlicht 0, auch wenn dieser Kunde in dem Mandanten Mandant2 einen Saldo hat.

Die Lösung für das CalcFields Problem ist recht einfach: Das Calcfield muss „einfach“ über die entsprechende Tabelle emuliert werden, also z.B.

CustLedgEntry.setrange(„Customer No.“, „Kundennummer“);
CustLedgEntry.Calcsums(Amount);

Doch wie verfahren wir mit dem Artikelbuchungsblatt oder jedem anderen Buchungsblatt? Oder generell beim Einfügen von Records? Records können ja z.B. mit Templates initialisiert werden. Da die Standard Navisionlogik keinen Mandantenwechsel für Trigger durchführt, müsste JEDE relevante Logik mit dem nötigen Mandantenwechsel nachprogrammiert werden. Ein einfaches Table.ChangeCompany(NeuerMandant, Global=True)“ würde uns das Leben so unglaublich viel einfacher machen…

Nun… das gibt es leider nicht. Nun… Ein bisschen schon!

Kompletter Mandantenwechsel mit allen Triggern!

StartSession(SessionID,CodeunitNo,Company,Record) erlaubt es, eine beliebige Codeunit als eine neue Sitzung zu starten – in einem beliebigen Mandanten. Da der Mandant (=Company) ja für eine Sitzung/Session gilt, gilt der als Parameter mitgegebene Mandant dan für die gesammte Laufzeit dieser Codeunit – und alle in oder durch diese Codeunit aufgerufenen Objekte, Trigger etc.

Der Trick liegt also darin, in dem obigen Beispiel für den Import die einzulesenden Dateien ganz ohne Trigger in die jeweiligen temporären Tabellen pro Mandant einzulesen, und diese temporäre Tabelle dann an eine Startsession zu übergeben.

Achtung! Wenn das in einem Rutsch passieren soll, dann braucht man auch temporäre Tabellen (z.B. als Array), oder man muss den gültigen Mandanten in der Tabelle mitspeichern, und in der aufgerufenen Codeunit dann korrekt reagieren!
Also


Case AktuelleZeileMandant of
'1' : TempTableMandant1.Feldwert = Irgendwas;
'2' : TempTableMandant2.Feldwert = Irgendwas;
end;

oder

TempTableMandant[AktuelleZeileMandant].Feldwert = Irgendwas;

Viel eleganter geht es allerdings, indem die komplette Einleselogik / Verarbeitungslogik direkt in die neue Session verlagert wird:

SessionStartetAt := CREATEDATETIME(WORKDATE,TIME);
STARTSESSION(SessionEvent."Session ID",50010,pCompanyCode,ImportAS400);
Log.Log(ObjRef,'SessionStart'+ pCompanyCode,SessionEvent."Session ID",'');
SLEEP(100);
ActiveSession.SETRANGE("Session ID",SessionEvent."Session ID");
WHILE ActiveSession.FINDFIRST DO BEGIN
SessionDuration := ROUND((CREATEDATETIME(WORKDATE,TIME) - SessionStartetAt) / 1000,1,'<');
SessionWindow.UPDATE(2,ROUND(SessionDuration / pEstDurationS * 10000,1,'<'));
SessionEvents.SETRANGE("Session Unique ID",ActiveSession."Session Unique ID");
SLEEP(1000);
END;
Log.Log(ObjRef,'Session Duration '+pCompanyCode,SessionDuration,pFileName);
SessionEvents.SETCURRENTKEY("Session Unique ID");
IF SessionEvents.FINDLAST THEN
Log.Log(ObjRef,'SessionEnd '+ pCompanyCode,SessionEvents.Comment,pFileName);

Das ist natürlich nur das Grundgerüst. Wenn Sie hier Bedarf haben, sprechen Sie mich bitte an.
Gerade im Bereich von AS/400 Datenübernahmen besteht hier oft der Bedarf, Tabellen, welche Mandantenübergreifend ausgegeben werden, in Navision Financials oder Business Central in getrennte Mandanten einzulesen. Bedenken Sie bitte, das Sie a) grundsätzlich keine Standard-Navision-Tabelle auf „Mandantenübergreifend = Ja“ stellen sollten. Sie öffnen damit die Büchse der Pandora. Und b) Dass Sie das seit BC 15 auch gar nicht mehr können… zum Glück.

Aber… wie weiß man denn, wann diese Session fertig ist?

Das ist das automatisch folgende Problem mit den Sessions. Sie sind autark. Sie arbeiten selbstständig. Wenn Sie fertig sind, sind sie fertig.
Das einfachste ist: Die zu verarbeitenden Tabellen so aufzubauen, dass man Sie mit Startsession abfeuern kann, und irgendwann sind sie halt fertig, und gut so. Das passt natürlich nicht, wenn die einzulesenden Daten aufeinander aufbauen. Z.B. erst die Artikelstammdaten, dann die zugehörigen Bewegungsdaten.

Wenn man nun aber unbedingt wissen will, wann sie fertig sind, braucht man so etwas wie einen Rückgabewert. Den Liefert StartSession sogar: OK := Startsession(…
Allerdings liefert OK nur, ob die Session gestartet werden konnte, nicht ob sie korrekt beendet wurde. Dies ist also kein Vergleich zu dem bekannten OK=Codeunit.run…, welcher ja a) abwartet, ob die Codeunit beendet wurde, und b) dazu auch noch ein Ergebnis liefert: TRUE: Korrekt abgearbeitet FALSE: Es trat ein Fehler in der Verarbeitung auf, welcher zu einem Error geführt hätte. So etwas gibt es bei StartSession nicht.

Nun… Zum Glück hat Navision / Business Central noch genug Wurzeln von Navision, damit man auch um den immer komplizierter werdenden Microsoft Teil noch einen Workaround bilden kann.

Die mit Startsession in Navision oder Business Central gestartete Codeunit trägt einen neuen Wert in der Sessionstabelle ein! Und bei Beendigung einen Eintrag in die SessionEvents, wenn vorhanden mit Abbruchfehlermeldung.

Hierzu gleich ein Tipp: Die beiden bereits vorhandenen Pages 670 & 9506 sowie eine neue Page auf die Tabelle 2000000111 Session Event direkt in die Menuesite unter Anwendungstools eintragen:

Screenshot vom Navision / Business Central RTC mit den Sitzungsinformationen (Sessioninfos) der Seiten 670, 9506 und der nicht im Standard vorhandenen Session Eventtabelle
Beispiel für die Erweiterung des Tool-Menü von Navision / Business Central RTC mit den Sitzungsinformationen (Sessioninfos) der Seiten 670, 9506 und der im Standard nicht anzeigbaren Session Eventtabelle

In dem obigen Beispiel ist das Warten auf das Beenden der frisch gestarteten Mandantenspezifischen Session bereits eingebaut!