Universeller Mandantenexport Import

Nav123: Navision, Showare, OrderApp

Navision und Business Central bieten bereits verschiedene Wege an, Mandanten auszuspielen und -im Prinzip- diese auch in eine andere Datenbank zu importieren. Leider funktionieren diese oft nicht wie gewünscht, erwartet oder erhofft. Hier biete ich Ihnen eine einfache (!) UND universelle Möglichkeit. Wunder kann aber auch diese natürlich nicht erbringen. Für Wunder in Navision, Dynamics, Financials und Business Central müssen Sie mich schon direkt kontaktieren.

Anwendungsfälle (Beispiele)

Cronus Mandant retten oder wiederherstellen

Das war die ursprüngliche Anforderung an diese Funktion. In einer Kundendatenbank hatte das Systemhaus den Cronus-Mandanten gelöscht. Warum? Keine Ahnung. Das war auf jeden Fall eine dumme Idee. Ich hatte auch schon Kunden, die die Cronus AG aus Versehen gelöscht haben. Und dann stellt man (auch ich) fest, dass so ein einzelner Mandant gar nicht so einfach wieder herzustellen ist! Also lebt man damit, dass die Cronus AG gelöscht bleibt. Jedoch: Das muss nicht so sein!

Testumgebung erstellen

Dafür würde ich diese Funktion nicht benutzen, Es gibt hier und hier eine bessere Methode dafür. Aber wenn es Sie glücklich macht…

Übertragen einer angepassten Datenbank in eine Standarddatenbank

Das ist in der Tat eine sehr praktische Verwendung! Angenommen, Sie haben eine sehr unglücklich verbogene Datenbank von Navision oder Business Central, und möchten nun unbedingt auf eine Standard-Navisionversion in der gleichen Releaseversion wechseln. Mein Universeller Export gibt alle Datenfelder aller Tabellen aus, die vorgefunden werden. Der zugehörige universelle Import hingegen liest nur die Felder in die Tabellen ein, welche in der Zieldatenbank vorgefunden werden. Ich denke, schon diese Funktion wird viele Wünsche wahr werden lassen. Natürlich gibt es hier auch massive Grenzen. Z.B. werden Sie eine Food-Vision Version, egal welche, niemals in einem Standard zum Laufen bewegen. Die Anpassungen (wenn man die noch so nennen kann), die hier von Modus vorgenommen wurden, greifen auch in den Standard von Navision und Business Central so tief ein, dass hier schon Intensivmedizin notwendig ist. Dies gilt natürlich nicht nur für Foodvision, sondern für sehr viele Branchenlösungen. Leiden auch Sie an einer schlimm verbastelten Branchenlösung?

Updaten von Navision über Versionsgrenzen hinweg

Oft sind die Unterschiede zwischen Major-Releaswechsel (z.B. zwischen Navision Dynamics 2015 und Navision Dynamics 2016) nicht sooo groß, da kann man mal probieren, wie weit man mit diesem Holzhammer kommt. Es gibt aber auch Versionswechsel, welche so massiv in die Datenstruktur eingreifen, dass man mindestens noch viele Nacharbeiten an den übernommenen Daten vornehmen muss. So konnte man z.B. in den früheren Navision-Versionen noch problemlos die Tabelle 339 Artikelausgleichsposten einfach löschen. In späteren Versionen ist die Tabelle 339 Artikelausgleichsposten lebensnotwendig für die -immer wieder zu Heiterkeit führenden- Lagerregulierung. Auch die Umstellung auf die Wertposten und die Debitor/Kreditorenpostendetails würden eine lauffähige Übertragung von VOR Wertposten-Datenbanken auf NACH Wertpostendatenbanken unmöglich machen. Aber vielleicht reicht es ja, um einen Uralten Mandanten auch in einem Ultramodernen Navision / Business Central zumindest ansehen zu können? Wenn Sie diese Funktion hier anwenden, werden Sie schon wissen, was Sie tun… ansonsten kontaktieren Sie mich besser! Vielleicht wollen Sie sich aber auch nur ihre 2009R2 Datenbank mal im RTC ansehen, ohne viel Aufwand? Den Cronus haben Sie ja sowieso in einer neuen Demoinstallation. Mit diesem kleinen Tool hier können Sie aber auch einfach mal einen echten Mandanten übertragen, diverse Statistiken, Auftragserfassung, Debitorenkarte und vieles mehr wird zumindest ein bisschen funktionieren.

Manipulation von Daten

Ja… eine sehr interessante Nutzungsoption. Mandant ausgeben, Daten modifizieren, Mandant zurücklesen… Ich rate dringend davon ab!

Ausgabe aller Daten für Analysen

Das kann wieder ganz praktisch sein. Sie geben mit einem Rutsch alle Daten, die ihr Navision kennt, in die Datendateien aus, uns binden die gewünschten Daten dann z.B. über einen CSV-Treiber in Access oder Excel ein. Aber auch hier würde ich andere Methoden wählen.

Ausgabe aller Daten für Systemwechsel

Echt? Sie möchten Navision / Business Central ablösen? Warum? Tun Sie das nicht! Es heißt zwar, man solle Reisende nicht aufhalten, aber bei Navision und Business Central bin ich da nicht so glücklich. Vermutlich haben Sie bisher schlechte Erfahrungen mit Navision oder Business Central gemacht? Wollen wir es einmal miteinander versuchen?

Bisherige Möglichkeiten, Mandanten aus Navision / Business Central auszugeben und einzulesen

In eine Datendatei exportieren / Aus einer Datendatei importieren

Haben Sie die Funktion(en) In eine Datendatei exportieren bzw. Aus einer Datendatei importieren bereits einmal unter dem RTC aufgerufen? Wenn Sie schon länger dabei sind: Kam ihnen das irgendwie bekannt vor? Richtig! Das ist die alte Datensicherung, die Sie im alten Navision über Extras/Datensicherung erstellen aufrufen konnten!

Ansicht der Funktion "In eine Datendatei exportieren" im RTC: Das ist die alte Datensicherung!
„In eine Datendatei exportieren“ im RTC: Das ist die frühere Datensicherung!
Ansicht der Funktion "Datensicherung erstellen" im alten 2009er Client: Das ist die Basis für "In eine Datendatei exportieren"!
Screenshot der Funktion „Datensicherung erstellen“ im alten 2009er Client: Das ist die Basis für „In eine Datendatei exportieren“!

Und damit haben Sie auch die gleichen Einschränkungen. Wenn Sie hier nur einen einzigen Mandanten ausgeben (das kleinste an Daten), und diesen dann in eine andere Datenbank einlesen möchten, so müssen Quell- und Zieldatenbank 100% (eher 110%) identisch sein. Sogar Extensions müssen absolut identisch sein, selbst wenn diese gar keine Änderung an Datenstrukturen vornehmen oder nicht aktiv sind. Dazu werden noch die Daten beim Export kodiert, so dass keine Klartextbearbeitung möglich ist.

Für alle oben beschriebenen Anwendungsfälle ist eine Übertragung von Mandanten aus einer Datenbank in eine andere Datenbank daher mit dieser Funktion praktisch nicht möglich.

Dies gilt auch für den entsprechenden Powershell-Befehl, welcher die gleiche Navision-Funktion aufruft.

Export-NAVData -ServerInstance <service name> -Tenant <tenant ID> -CompanyName "<Company name>" -FilePath <location>

Technisch ist übrigens sowohl der Powershell-Befehl wie auch die Funktion In eine Datendatei exportieren nichts anderes als der Aufruf des (relativ neuen) Befehls [Database.]ExportData:

Aufruf des Navision / Business Central Befehls ExportData direkt über den Programmcode erzeugt die gleichen Dateien wie In Datendatei exportieren, faktisch eine alte Navision Datensicherung.

Wenn Sie diese Seite hier gesucht und gefunden haben, wissen Sie sicherlich schon, dass dies für die beschriebenen Anwendungsfälle nicht geeignet ist.

Konfigurationspakete / Rapidstart / Datenexporte (früher GdPDU Export)

Datenexporte lasse ich hier mal weg, die sind zu spezifisch, können aber durchaus auch das geeignete Werkzeug sein! Früher hießen die GdPDU Export, was schon einen Hinweis auf den eigentlichen Zweck gibt.

Die Konfigurationspakete (alter Name: Rapidstart) sind ein unglaublich geiles Werkzeug, welches über die verschiedenen Versionen eine echte eierlegende Wollmilchsau geworden ist.
Sie können damit ALLE Daten aus Navision in Excel ausgeben (na ja… „alle“ bis zu 2 Mio Datensätze…), und viele Daten auch wieder zurück lesen. „Viele“ beschränkt sich auf nicht schreibgeschützte Tabellen. Also keine gebuchten Belege wie Verkaufsrechnungen, Lieferscheine, registrierte Mahnungen… und gebuchten Posten (Artikelposten, Wertposten, Debitorenposten…). Davon ab empfiehlt es sich aber unbedingt, dass Sie sich mit den Konfigurationspaketen auseinandersetzen, wenn Sie das noch nicht getan haben. Um mal schnell einen Cronus wieder herzustellen oder mal schnell einen Mandanten aus einer alten Navision-Version in eine neue Version zu kopieren, ist dieses Werkzeug nicht geeignet. A) liegt es oft nicht in der älteren Version vor, B) manchmal unterscheidet sich der interne Aufbau der ausgegebenen Dateien zwischen verschiedenen Konfigurationspaketen / Rapidstart Versionen von Navision, und C) ist es schlicht viel zu aufwändig, alle Tabellen, die man braucht, dort einzutragen. Na ja, und D) natürlich auch wieder, dass dieses Tool nicht in geschützte Bereiche wie gebuchte Rechnungen oder direkt in Debitorenposten exportieren/importieren kann.

Copy & paste

Ja, tatsächlich! Im alten Navision können Sie direkt über die Tabellen (Designer, Table, Run) sehr viele Tabellendaten (alle Daten von Navision liegen in irgendwelchen Tabellen!) aus der einen Tabelle/Datenbank per STRG+A (Alles Markieren), STRG+C (Copy, in Navision RTC und Business Central STRG+Shift+C) kopieren, und dann in der anderen Navision-Datenbank mit STRG+V (Insert, das V erinnert an einen Trichter, in Navision RTC und Business Central STRG+Shift+V) einfügen.
Allerdings müssen hier die jeweils betroffenen Tabellen auch wieder 100% identisch sein (bei RTC nicht mehr ganz so 100%), und je nach Anzahl Datensätze dauert das Kopieren (nicht das Einfügen!) eeeeelendig lange. Ein Nachteil sei extra erwähnt: Es werden alle Tabellentrigger und Prüfungen durchlaufen, daher spielt die Reihenfolge der Spalten auf dem Bildschirm und die Reihenfolge, in der die Tabellen eingefügt werden, eine große Rolle. Und z.B. ist danach das „Korrigiert am“ Datum immer ein aktuelles Datum. Hier oder da ist Kopieren und Einfügen aber tatsächlich eine praktische Sache, die mal eben viel Zeit sparen kann.

Arbeitsweise meines universal Mandantenexport & Mandantenimport

Im Prinzip habe ich einfach einen Serializer und De-Serializer geschrieben, welcher einfach nacheinander alle Tabellen durchläuft, pro Tabelle eine Datei anlegt, und danach je Datensatz eine Zeile in diese Datei schreibt. Dabei werden leere Felder, Flowfields und Flowfilter automatisch überprungen, was die Datei(en) etwas schlanker macht. Per Anpassungen in der Funktion können natürlich auch noch Tabellen eingeschränkt werden.
Der Deserializer wiederum kann dieses Datenformat verstehen, er legt dann pro Zeile in der Datei in der richtigen Tabelle wieder einen Datensatz an.

Nebenbei ist das auch ein schönes Beispiel für die Verwendung von Recordref (recRef) und Fieldref, was den Programmcode erstaunlich kurzhält.

Jede einzelne benutzte Tabelle von Navision / Business Central wird in eine einzelne Datendatei ausgegeben
Der Export schreibt für jede einzelne benutzte Tabelle von Navision / Business Central in eine einzelne Datendatei. Auf diese Weise kann z.B. auch vor einem Import einfach eine Datei gelöscht werden (z.B. die Artikelpostentabelle). Diese wird dann im Zielmandanten einfach nicht mehr eingelesen/importiert.

Es werden keine BLOB-felder ausgegeben, so dass z.B. in dem importierten Mandanten keine Artikelbilder etc. zu sehen sind. Wenn Sie das benötigen, melden Sie sich bitte bei mir. Sie können die Funktion aber selbst einfach um den Binärexport/Import erweitern. Technisch wird dann einfach pro BLOB-Feld Inhalt eine eigene Datei mit dem Binär-Inhalt ausgegeben und später wieder eingelesen.

Diese Funktion ist mit dieser Veröffentlichung „Free to use“, selbstverständlich ohne irgendwelche Funktionsgarantien oder Sicherheiten.
Eine Verwendung ist NICHT gestattet, auch nicht auszugsweise, für die Firma Landefeld Druckluft & Pneumatik aus Kassel und für Dr. Ulrich Obermüller (Wohnort evtl. Kiel). Zu beiden gibt es eine Vorgeschichte, die ich bei Gelegenheit auch noch veröffentliche. Mit beiden habe ich unglaublich schlechte Erfahrungen gemacht.

Geeignete Navision bzw. Business Central Versionen

Ohne RecordRef geht es nicht. Recordref bzw. RecRef wurde mit der 3.60er, stabil ab der 3.70er Navision Version eingeführt. Die gute Nachricht: Ältere Navision-Datenbanken, z.B. von 2.01(b) etc. können sehr einfach in eine 2009 oder 2009 R2 oder 4er/5er konvertiert werden.
Außerdem wurde von der mit dem RTC (Navision 2013, 2015 etc) eingeführten unbegrenzten Stringlänge profitiert. Hier könnten aber einfach für ältere Versionen die Datenfelder Zeilenweise statt in einer Zeile ausgegeben werden. Weitere Einschränkungen gibt es nicht, so könnten z.B. alle Tabellen & Felder, die in Navision 2009R2 und Navision 2018 (Business Central BC 14) identisch sind, auch von 2.01b oder 2009R2 nach BC14 übertragen werden. Beim Import werden automatisch alle Felder, die nicht zugeordnet werden können, ignoriert. Dabei werden kleinere Änderungen, z.B. das Ändern von einem Integer-Feld auf ein Code-Feld, automatisch vorgenommen.
Spoileralarm: Mehr als „ein bisschen gucken“ wird mit dieser Datenbank dann sicherlich nicht gehen! Aber das ist ja schon mehr, als mit offiziellen Tools überhaupt geht.

Grundsätzlich könnte damit auch ein 3.56er Navision in ein 2.01er Navision eingelesen werden, das dann zu 2009R2 konvertiert werden, und so weiter. Aber! Das brauchen Sie gar nicht! Sie können eine Navision 3.53, 3.56, 3.5x Datensicherung einfach mit einem 2.01(b) Navision direkt einlesen! Dabei werden sogar alle Tabellen und alle Felder, die fehlen, automatisch von Navision in der vorher leeren Datenbank angelegt. Es wird allerdings keine Prozesslogik eingelesen, sondern nur die Daten. Das können Sie dann mit einem Fingerschnips zu 2009R2 konvertieren (na ja… fast…), und dann mal schnell Ihre Stammdaten unter RTC oder im Webclient ansehen.

Der Code wird hier so veröffentlicht, wie er funktionierte. Er wird hier nicht gepflegt und erweitert! Wenn Sie sich für diese Funktion interessieren, aber sich noch mehr davon erhoffen, als nur mal schnell eine Cronus AG wiederherzustellen, dann melden Sie sich bitte bei mir.

OBJECT Codeunit 50003 RTH_ExportImportCompany
{
  OBJECT-PROPERTIES
  {
  }
  PROPERTIES
  {
    Permissions=TableData 17=id,
                TableData 21=id,
                TableData 25=id,
                TableData 32=id,
                TableData 45=id,
                TableData 46=id,
                TableData 110=id,
                TableData 111=id,
                TableData 112=id,
                TableData 113=id,
                TableData 114=id,
                TableData 115=id,
                TableData 120=id,
                TableData 121=id,
                TableData 122=id,
                TableData 123=id,
                TableData 124=id,
                TableData 297=id,
                TableData 298=id,
                TableData 339=id,
                TableData 379=id,
                TableData 380=id,
                TableData 405=id,
                TableData 480=id,
                TableData 5802=id,
                TableData 5811=id,
                TableData 9004=id,
                TableData 9006=id,
                TableData 9008=id,
                TableData 9999=id;
    OnRun=VAR
            TXTImportExport@1000000000 : TextConst 'DEU=Firma %1 in Datei exportieren,Datei in Mandant %1 migrieren,Mandant %1 mit Datei initialisieren;ENU=Export company %1 to File,Migrate File into Company %1,Init Company %1 with File';
            ConfirmString@1000000001 : Text;
          BEGIN
            ConfirmString := STRSUBSTNO(TXTImportExport,COMPANYNAME);
            CASE STRMENU(ConfirmString) OF
              1 : ExportCompany;
              2 : ImportCompany(FALSE);
              3 : ImportCompany(TRUE);
            END;
          END;

  }
  CODE
  {
    VAR
      Tables@1000000002 : Record 2000000028;
      SystemObject@1000000008 : Record 2000000029;
      RecRef@1000000000 : RecordRef;
      TXTbasicPath@1000000003 : TextConst 'DEU=c:\temp\';
      Window@1000000004 : Dialog;
      DataFile@1000000001 : File;
      RecCounter@1000000005 : Integer;
      TXTextension@1000000007 : TextConst 'DEU=NavCompany';
      TXTdivider@1000000006 : TextConst 'DEU=Ý';

    LOCAL PROCEDURE ExportCompany@1000000000();
    VAR
      FileName@1000000000 : Text;
    BEGIN
      Tables.SETRANGE("Table No.",1,1999999999);
      Tables.SETFILTER("No. of Records",'>0');
      Tables.SETRANGE("Company Name",COMPANYNAME);
      Window.OPEN('Export #1########################################\@2@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@');
      IF Tables.FINDSET THEN REPEAT
        FileName := TXTbasicPath + FORMAT(Tables."Table No.") + '_'+ DELCHR(Tables."Table Name",'=','/\?*+') +'.'+ TXTextension;
        Window.UPDATE(1,FileName);
        CLEAR(RecRef);
        RecRef.OPEN(Tables."Table No.");
        IF RecRef.READPERMISSION THEN
          IF RecRef.FINDSET THEN BEGIN
            CLEAR(DataFile);
            DataFile.TEXTMODE(TRUE);
            DataFile.CREATE(FileName,TEXTENCODING::UTF8);
            CLEAR(RecCounter);
            REPEAT
              RecCounter += 1;
              IF RecCounter MOD 1000 = 0 THEN BEGIN
                Window.UPDATE(2,ROUND(RecCounter / Tables."No. of Records" * 10000,1,'<'));
                CLEAR(RecCounter);
              END;
              DataFile.WRITE(ExportRecord(RecRef));
            UNTIL RecRef.NEXT = 0;
            DataFile.CLOSE;
          END;
      UNTIL Tables.NEXT = 0;
      Window.CLOSE;
    END;

    LOCAL PROCEDURE ExportRecord@1000000002(pRecRef@1000000000 : RecordRef) RecordString : Text;
    VAR
      FieldRef@1000000001 : FieldRef;
      FieldCounter@1000000002 : Integer;
      FieldAsText@1000000003 : Text;
    BEGIN
      FOR FieldCounter := 1 TO pRecRef.FIELDCOUNT DO BEGIN
        FieldRef := pRecRef.FIELDINDEX(FieldCounter);
        IF FORMAT(FieldRef.CLASS) = 'Normal' THEN BEGIN
          CLEAR(FieldAsText);
          CASE FORMAT(FieldRef.TYPE) OF
            'BLOB','Binary','Media','MediaSet':;
          ELSE
            FieldAsText := FORMAT(FieldRef,0,9);
          END;
          FieldAsText := CONVERTSTR(FieldAsText,TXTdivider,'_');
          IF FieldAsText <> '' THEN
            RecordString += TXTdivider + FORMAT(FieldRef.NUMBER) +';'+FieldAsText;
        END;
      END;
    END;

    LOCAL PROCEDURE ImportCompany@1000000001(pInitTables@1000000000 : Boolean);
    VAR
      FileList@1000000001 : Record 2000000022;
      TableNo@1000000002 : Integer;
      TXTallTables@1000000003 : TextConst 'DEU="Sollen ALLE Tabellen gel”scht werden? Nein=nur die eingelesenen.";ENU="Clear ALL Tables? No=Only imported."';
      DataFileLen@1000000004 : Integer;
      DataFileLine@1000000005 : Text;
    BEGIN
      IF pInitTables THEN
        IF CONFIRM(TXTallTables) THEN BEGIN
          Window.OPEN('Delete Table #1###########');
          Tables.SETFILTER("No. of Records",'>0');
          Tables.SETRANGE("Table No.",1,1999999999);
          IF Tables.FINDSET THEN REPEAT
            Window.UPDATE(1,Tables."Table No.");
            CLEAR(RecRef);
            RecRef.OPEN(Tables."Table No.");
            RecRef.DELETEALL;
          UNTIL Tables.NEXT = 0;
          Window.CLOSE;
          COMMIT;
        END;

      Window.OPEN(
        'Import #1########################################\'+
        '@2@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\'+
        '#3########################################');
      FileList.SETRANGE(Path,TXTbasicPath);
      FileList.SETFILTER(Name,'@*'+TXTextension);
      FileList.SETFILTER(Size,'>0');
      IF FileList.FINDSET THEN REPEAT
        Window.UPDATE(1,FileList.Name);
        EVALUATE(TableNo,COPYSTR(FileList.Name,1,STRPOS(FileList.Name,'_')-1));
        CLEAR(RecRef);
        RecRef.OPEN(TableNo);
        IF RecRef.WRITEPERMISSION THEN BEGIN
          DataFile.TEXTMODE(TRUE);
          DataFile.OPEN(FileList.Path + FileList.Name,TEXTENCODING::UTF8);
          DataFileLen := DataFile.LEN;
          WHILE DataFile.POS < DataFileLen DO BEGIN
            IF DataFile.POS MOD 1000 = 0 THEN
              Window.UPDATE(2,ROUND(DataFile.POS / DataFileLen * 10000,1,'<'));
            DataFile.READ(DataFileLine);
            ImportRecord(RecRef,DataFileLine);
          END;
          DataFile.CLOSE;
        END;
        COMMIT;
      UNTIL FileList.NEXT = 0;
      Window.CLOSE;
    END;

    LOCAL PROCEDURE ImportRecord@1000000003(pRecRef@1000000000 : RecordRef;pDataLine@1000000001 : Text);
    VAR
      FieldRef@1000000002 : FieldRef;
      FieldNo@1000000003 : Integer;
      FieldContent@1000000004 : Text;
      FieldContentPosition@1000000005 : Integer;
      FieldContentLen@1000000006 : Integer;
      RecCounter@1000000007 : Integer;
    BEGIN
      WHILE STRLEN(pDataLine) > 0  DO BEGIN
        IF COPYSTR(pDataLine,1,1) = TXTdivider THEN
          pDataLine := COPYSTR(pDataLine,2);
        EVALUATE(FieldNo,COPYSTR(pDataLine,1,STRPOS(pDataLine,';')-1));
        IF pRecRef.FIELDEXIST(FieldNo) THEN BEGIN
          FieldRef := RecRef.FIELD(FieldNo);
          FieldContentPosition := STRPOS(pDataLine,';')+1;
          IF STRPOS(pDataLine,TXTdivider) > 0 THEN BEGIN
            FieldContentLen := STRPOS(pDataLine,TXTdivider) - FieldContentPosition;
            FieldContent := COPYSTR(pDataLine,FieldContentPosition,FieldContentLen);
            pDataLine := COPYSTR(pDataLine,STRPOS(pDataLine,TXTdivider)+1);
          END ELSE BEGIN
            FieldContent := COPYSTR(pDataLine,STRPOS(pDataLine,';')+1);
            CLEAR(pDataLine);
          END;
          IF EVALUATE(FieldRef,FieldContent) THEN;
        END;

      END;
      IF pRecRef.INSERT THEN BEGIN
        RecCounter += 1;
        IF RecCounter = 100 THEN BEGIN
          Window.UPDATE(3,pRecRef.GETPOSITION);
          CLEAR(RecCounter);
        END;
      END;
    END;

    BEGIN
    END.
  }
}