Auch in aktuellen Versionen kommen wir immer wieder an unsere Grenzen wenn es um die maximale Datensatzgröße in Tabellen geht.
Diese zwei VBA-Funktionen helfen mir im Excel die aktuelle Größe einer Tabelle zu bestimmen.

Option Explicit
Function CalculateSize(ByVal FieldClass As String, ByVal FieldType As String, ByVal FieldLength As Integer)
    Dim FieldSize As Single
    If FieldClass <> "Normal" Then
      CalculateSize = 0
      Exit Function
    End If
    Select Case FieldType
      Case "Option", "Integer", "Boolean"
        FieldSize = 4
      Case "BigInteger", "Date", "DateTime", "Duration", "Time", "BLOB"
        FieldSize = 8
      Case "Decimal"
        FieldSize = 12
      Case "DateFormula"
        FieldSize = 32
      Case "GUID"
        FieldSize = 16
      Case "Code"
        FieldSize = ((FieldLength + 1) * 2) + 1
      Case "Text"
        FieldSize = (FieldLength + 1) * 2
      Case Else
        FieldSize = 0
    End Select
    CalculateSize = FieldSize
End Function
Function CalculateSizeClassic(ByVal FieldClass As String, ByVal FieldType As String, ByVal FieldLength As Integer)
    Dim FieldSize As Single
    If FieldClass <> "Normal" Then
      CalculateSizeClassic = 0
      Exit Function
    End If
    Select Case FieldType
      Case "Option", "Integer", "Boolean", "Date", "Time"
        FieldSize = 4
      Case "BigInteger", "DateTime", "Duration", "BLOB"
        FieldSize = 8
      Case "Decimal"
        FieldSize = 12
      Case "DateFormula"
        FieldSize = 32
      Case "GUID"
        FieldSize = 16
      Case "Code"
        FieldSize = WorksheetFunction.Ceiling((FieldLength + 2), 4)
      Case "Text"
        FieldSize = WorksheetFunction.Ceiling((FieldLength + 1), 4)
      Case Else
        FieldSize = 0
    End Select
    CalculateSizeClassic = FieldSize
End Function

 

Nachdem nun auch Dynamics in der Dockerwelt ankommt, habe ich mich die letzten Tage mit dem Thema Docker als Ablösung für Virtuelle Maschienen beschäftigt.

In diesem Beitrg möchte ich lediglich meine ersten Schritte aufzeigen.

 

Schritt 1 war hierbei die Installation von Docker.

if (-not (Test-IsAdmin))
{ 
  Write-Error "Skript erfordert Admin-Rechte!";
  return 
}
Set-Location ([io.path]::GetTempPath())
Invoke-WebRequest https://download.docker.com/win/stable/Docker%20for%20Windows%20Installer.exe -OutFile DockerInstaller.exe
Start-Process DockerInstaller.exe -wait | Write-Verbose
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V, Containers -All
Install-Module Docker -Force

Hierbei wird die Docker MSI in einen temprären Ordner heruntegeladen und gestgartet.

Im Anschluss installiere ich ich noch die beiden Windows-Features Hyper-V und Containers.

Beide sollten bereits durch die MSI installiert worden sein, aber sicher ist sicher.

Auch die Powershell-Module für Docker werden im Anschluss gleich mit installiert.

Ab jetzt ist es mir möglich über Request-ContainerImage aus Powershell heraus neue Container-Images abzurufen bzw. zu aktualisieren.

In meinen Augen hat Docker zwei Stufen.

Es gibt Container-Images, das sind vorgefertigte Templates.

Aus diesen Container-Images kann man nun Container erstellen, diese sind eine Kopie des Images und bekommen durch Docker bei der Anlage einen eindeutigen Namen.

 

Kommen wir nun zu Schritt 2, dem starten eines SQL-Containers.

Request-ContainerImage microsoft/mssql-server-windows-express

Über Powershell lade ich das Standard-Image für MSSQL herunter.

Und im Anschluss erzeuge ich mir einen neuen Container auf dieser Grundlage.

docker run --ip 172.22.10.1 -d -p 1433:1433 --env sa_password=123456 --env accept_eula=y microsoft/mssql-server-windows-express

Über –ip vergebe ich dem Container eine feste IP, mit -d lasse ich ihn im Hintergrund laufen.

Durch -p 1433:1433 wird der Port 1433 auf 1433 gemappt. (1433 ist der Standardport für MSSQL)

Mit Hilfe von –env vergbe ich noch ein Passwort (123456) für den sa-Benutzer und akzeptiere die EULA.

In der Ausgabe erhalte ich nun den eindeutigen Namen meines Containers.

Bei mir ist dies romantic_hermann.

Ab jetzt bin ich in der Lage mich z.B. über das SQL Server Managment Studio meines loaklen Rechners (Host) auf den SQL-Server innerhlab des Container zu verbinden.

Die IP-Adresse ist 172.22.10.1, der Benutzer ist sa, das Kennwort 123456.

Hier eine Auflistung der benötigten Versionen und die Links zu den Anleitungen von Clausl.

 

NAV Version Visual Studio
 2009  2005 & 2008 (ab SP1 stabil)
2009 R2 2008
2013 2010 SP1
2013 R2 2012
2013 R2 > CU2 2013
2015 2013
2016 2013

 

Free Visual Studio Designer for editing Reports in Dynamics NAV 2013 and other options

Free Visual Studio designer for NAV 2015 and NAV 2013 R2

Der mit NAV 2009 eingeführte Systemindikator kann neun verschiedene Akzente annehmen.

Wobei Akzente lediglich für die Farbgestaltung steht. Der Standard entspricht „Accent9“.

Da das Wechseln des Akzentes nur nach dem Neustart des RTC zu sehen ist, dauert das durchprobieren immer etwas.

Um mir das zu ersparen habe ich einmal alle Akzente in einer Grafik abgespeichert.

SI_Total

Viel Spaß damit Cheeze

In NAV gibt es seit Jahr und Tag eine feste Regel:

Gibt es keine Benutzer darf jeder die Applikation benutzen.

Das ist besonders dann hilfreich, wenn man gerade vom Kunden eine Datenbank bekommen hat, zu der man keinen Login kennt.

Und dank SQL haben wir auch dann noch die Möglichkeit die Benutzer los zu werden, wenn wir gar keinen der angelegten Benutzer kennen.

Am schnellsten geht das über ein paar SQL-Befehle:

USE [Datenbankname]
GO

Delete from [Access Control]
Delete from [User]
Delete from [User Metadata]
Delete from [User Personalization]
Delete from [User Property]
Delete from [User Default Style Sheet]

Anschliessend muss nur noch das Service-Tier neu gestartet werden.

Eine weitere immer wieder auftauchende Anforderung ist das automatische Splitten von Kommentaren.

Hier allerdings doch bitte nicht alle X Zeichen, sondern zwischen den Wörtern.

Eine Funktion hierfür kann z.B. so aussehen (Beispiel mit Sales Comment Line).

AddHeaderText(SalesHeader : Record "Sales Header";CommentCode : Code[10];NewText : Text;OrderLineNo : Integer)

Local Variables
Name			DataType	Subtype			Length
SalesCommentLine	Record		Sales Comment Line	
RemainingText		Text		
LastSpacePos		Integer		
SpacePos		Integer		
LineNo			Integer		

IF NewHeaderText = '' THEN
  EXIT; // NO TEXT -> NO COMMENT -> NO EMPTY LINES
// INSERT EMPTY COMMENT LINE BEFORE EXIT, IF YOU WANT TO ALLOW EMPTY LINES  
SalesCommentLine.SETRANGE("Document Type",SalesHeader."Document Type");
SalesCommentLine.SETRANGE("Document No.",SalesHeader."No.");
SalesCommentLine.SETRANGE("Document Line No.",OrderLineNo);
IF SalesCommentLine.FINDLAST THEN
  LineNo := SalesCommentLine."Line No."
ELSE
  LineNo := 0;
SalesCommentLine.RESET;
SalesCommentLine."Document Type" := SalesHeader."Document Type";
SalesCommentLine."Document No." := SalesHeader."No.";
SalesCommentLine."Document Line No." := OrderLineNo;
SalesCommentLine.Date := WORKDATE;
SalesCommentLine.Code := CommentCode;
RemainingText := NewText;
REPEAT
  IF STRLEN(RemainingText) > 80 THEN BEGIN // COMMENT HAS MORE THEN 80  CHARACTERS
    LastSpacePos := 0;
    REPEAT
      SpacePos := STRPOS(COPYSTR(RemainingText,LastSpacePos + 1),' '); // FIND NEXT SPACE
      IF LastSpacePos + SpacePos < 80 THEN
        LastSpacePos := LastSpacePos + SpacePos + 1;
    UNTIL (SpacePos = 0) OR (LastSpacePos + SpacePos >= 80);
	IF LastSpacePos = 0 THEN
		LastSpacePos = 80; // NO SPACE IN THE FIRST 80 CHARACTERS FOUND
    LineNo := LineNo + 10000;
    SalesCommentLine."Line No." := LineNo;
    SalesCommentLine.Comment := COPYSTR(RemainingText,1,LastSpacePos - 2);
    RemainingText := COPYSTR(RemainingText,LastSpacePos);
    SalesCommentLine.INSERT;
  END ELSE BEGIN // LESS THEN 80 CHARACTERS LEFT, NO MORE SPLITTING NEEDED
    LineNo := LineNo + 10000;
    SalesCommentLine."Line No." := LineNo;
    SalesCommentLine.Comment := RemainingText;
    SalesCommentLine.INSERT;
    RemainingText := '';
  END;
UNTIL RemainingText = '';

 

Diese Woche bin ich auf ein eher störendes Phänomen bei der Entwicklung in einer NAV 2013 R2 CU12 Datenbank gestosen. (Das verhalten konnte ich inzwischen auch in älteren Build-Versionen nachstellen)

Ich lege eine neue Tabelle an, erzeuge eine Page dazu und klicke auf RUN.

SQL-Fehler: Tabelle kann nicht gefunden werden.

Kurzer Blick auf dem Server, die Tabelle fehlt tatsächlich.

Export -> Lösche -> Import -> Tabelle fehlt

Sync-NAVTenant im Powershell aufrufen -> kein Fehler -> Tabelle fehlt

Neustart NST -> Tabelle fehlt -> Start RTC -> Tabelle wird angelegt

 

Diese Verhalten war Problemlos jederzeit reproduzierbar. Doch wo kam es her? An CU12 scheint es nicht zu liegen.

Fragen wir doch mal das Internet: Keine der üblichen Suchen bringt etwas das helfen könnte.

Dann hat es mir doch noch gedämmert: Es sieht so aus als ob der Trigger in der Object Metadata Tabelle fehlt.

Kurzer Blick: Stimmt, der ist nicht da.

 

Wie habe ich das den geschafft?

Mein Vorgehen war so:

– Neue Datenbank über SQL anlegen

– Import einer NAVDATA-Sicherung inkl. aller Objekte und Daten

 

In der Dev-Enviroment sieht alles gut aus. Der RTC läuft auch stabil.

 

Zum Schluß dann noch wie das ganze zu korrigieren ist:

Der Trigger lässt sich über SQL anlegen:

/****** Object: Trigger [dbo].[$ndo$objecttracking] Script Date: 23.10.2014 08:38:20 ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TRIGGER [dbo].[$ndo$objecttracking] ON [dbo].[Object Metadata] FOR DELETE, INSERT, UPDATE AS SET NOCOUNT ON DECLARE @delCount INTEGER, @insCount INTEGER, @updatedRows INTEGER, @currentDBTS BIGINT SELECT @delCount = COUNT(*) FROM DELETED SELECT @insCount = COUNT(*) FROM INSERTED SELECT @updatedRows = 0 SELECT @currentDBTS = CAST(@@DBTS AS BIGINT) IF (@insCount > 0) BEGIN UPDATE [dbo].[Object Tracking] SET [Object Timestamp] = @currentDBTS FROM INSERTED AS I JOIN [dbo].[Object Tracking] AS T ON (T.[Object Type] = I.[Object Type] AND T.[Object ID] = I.[Object ID] AND T.[Change Type] = 0) SELECT @updatedRows = @@ROWCOUNT IF (@updatedRows < @insCount) BEGIN INSERT INTO [dbo].[Object Tracking] ([Object Type], [Object ID], [Change Type], [Object Timestamp]) SELECT I.[Object Type], I.[Object ID], 0, @currentDBTS FROM INSERTED AS I LEFT OUTER JOIN [dbo].[Object Tracking] AS T ON (I.[Object Type] = T.[Object Type] AND I.[Object ID] = T.[Object ID] AND T.[Change Type] = 0) WHERE T.[Object Type] IS NULL AND T.[Object ID] IS NULL END END IF (@delCount > 0) BEGIN IF (@updatedRows = 0) BEGIN UPDATE [dbo].[Object Tracking] SET [Object Timestamp] = @currentDBTS FROM DELETED AS D JOIN [dbo].[Object Tracking] AS T ON (T.[Object Type] = D.[Object Type] AND T.[Object ID] = D.[Object ID] AND T.[Change Type] = 0) SELECT @updatedRows = @@ROWCOUNT END IF (@updatedRows < @delCount) BEGIN INSERT INTO [dbo].[Object Tracking] ([Object Type], [Object ID], [Change Type], [Object Timestamp]) SELECT D.[Object Type], D.[Object ID], 0, @currentDBTS FROM DELETED AS D LEFT OUTER JOIN [dbo].[Object Tracking] AS T ON (D.[Object Type] = T.[Object Type] AND D.[Object ID] = T.[Object ID] AND T.[Change Type] = 0) WHERE T.[Object Type] IS NULL AND T.[Object ID] IS NULL END END GO

Nachdem hier lange Zeit nichts neues kam, habe ich mich für eine Änderung entschieden:

Ab jetzt ist die Hauptsprache des Blogs Deutsch.

Das hat vor allem den Vorteil, dass neue Beiträge schneller geschrieben und veröffentlicht sind.

Eventuelle englische Übersetzungen können dann bei Nachfrage folgen.

Die nächsten Tage wird es auch gleich einige neue Beiträge zum Thema AddIn-Design für den WebClient geben.

Cheeze

When working with Data in Microsoft Dynamics NAV, there are some functions you do not need often, but if you need them, you will probably need to run them on many tables at once.

One example is truncating all the data in some tables. Yes, there is a function for that: DELETEALL.

But it is a lot of work to define a bunch of variables for the tables, name them in a speaking maner and use DELETEALL on the variable

Here is what you could do instead:

Use a new or existing Codeunit „Basic Functions“ and include this function:

TruncateTable(TableID : Integer)
recRef : RecordRef

recRef.OPEN(TableID);
RecRef.DELETEALL;

Now you just need to run this function.

It could look like this:

Basics.TruncateTable(50000);

Basics.TruncateTable(50001);

Basics.TruncateTable(50002);

Basics.TruncateTable(50003);

Basics.TruncateTable(50004);

Beim Arbeiten mit Daten in Microsoft Dynamics NAV, gibt es immer wieder Momente in denen man eine bestimmte Funktion braucht die einem der Standard entweder nicht zur Verfügung stellt, oder die nicht ganz ausreichend ist.

Ein Beispiel dafür ist das leeren eine Tabelle. Ja, mir ist bewusst das der Standard hier schon etwas hat: DELETEALL.

Allerdings kann es ziemlich viel Arbeit sein alle benötigten Tabellen als  sprechende Variablen anzulegen und anschliessend mit DELETEALL auf zu rufen.

Alternativ kannst du eine Codeunit „Basic Functions“ erstellen bzw. um folgende Funktion ergänzen:

TruncateTable(TableID : Integer)
recRef : RecordRef

recRef.OPEN(TableID);
RecRef.DELETEALL;

Jetzt brauchst du die Funktion nur noch aufzurufen (was genau eine Variable benötigt):

Basics.TruncateTable(50000);

Basics.TruncateTable(50001);

Basics.TruncateTable(50002);

Basics.TruncateTable(50003);

Basics.TruncateTable(50004);