", "").Replace("
", "").Replace("=", "_").Replace(">", "_").Replace("|", "_").Replace("!", "_").Replace("<", "_").Replace("/", "_")) + Return ReportFile + Else + Return Nothing + End If + Else + Return Nothing + End If + End Function + Public Function getFDSDoc(ByRef file As Byte(), reportid As String, type As String) As IO.FileInfo + Dim pl As New List(Of SqlClient.SqlParameter) From {SQL_VarChar("@type", type), SQL_VarChar("@reportid", reportid)} + Dim sqldt As SQLDataTable = System.Threading.Tasks.Task.Run(Async Function() Await getSQLDatatable_async("EXECUTE [dbo].[fds__getFDSDocument] @type, @reportid;", FDSConnectionString, SqlParameterList:=pl, options:=New fds_SQLOptions())).Result() + If sqldt.Count > 0 Then + Dim irow As DataRow = sqldt.FirstRow() + Dim fl As Byte() = irow.no("file", Nothing), fln As String = irow.nz("DocumentName") + If IsNothing(fl) = False Then + file = fl + Dim ReportFile As New IO.FileInfo(IO.Path.GetTempPath & fln.Replace("=", "_").Replace(">", "_").Replace("|", "_").Replace("!", "_").Replace("?", "_")) + Return ReportFile + Else + Return Nothing + End If + Else + Return Nothing + End If + End Function + + Public Function DATEV(header As DATEV_HEADER, tbl As DataTable) As String + + Return header.toHeaderstring() & ControlChars.CrLf & tbl.ToCsv(includeHeaders:=True, quoteStrings:=False, fieldDelimiter:=";", cultureinfo:=New Globalization.CultureInfo("de-DE"), quoteHeader:=False) + End Function + Public Class DATEV_HEADER + + 'c.f. https://developer.datev.de/portal/de/dtvf/formate/header + + Public Kennzeichen As DATEV_Kennzeichen = DATEV_Kennzeichen.EXTF + Public Versionsnummer As Integer = 700 + Public Formatkategorie As DATEV_Formatkategorie = DATEV_Formatkategorie.Debitoren__Kreditoren + Public Formatversion As DATEV_Formatversion = DATEV_Formatversion.Debitoren__Kreditoren + Public Beraternummer As Integer + Public Mandantennummer As Integer + Public WJBeginn As Date + Public Sachkontenlänge As Integer + Public Datum_von As Date + Public Datum_bis As Date + Public Bezeichnung As String + Public Buchungstyp As DATEV_Buchungstyp = DATEV_Buchungstyp.Finanzbuchführung + Public Rechnungsslegungszweck As DATEV_Rechnungslegungszweck = DATEV_Rechnungslegungszweck.unabhängig + Public Festschreibung As Boolean = 1 + Public WKZ As String = "EUR" + + + Public created As Date = Now() + Public ReadOnly Property Formatname As String + Get + Return [Enum].GetName(GetType(DATEV_Formatkategorie), Me.Formatkategorie).Replace("__", "/").Replace("_", " ") + End Get + End Property + + Dim EmptyDblQuote As String = Microsoft.VisualBasic.ChrW(34) & Microsoft.VisualBasic.ChrW(34) + + Public ReadOnly Property toHeaderstring As String + Get + Dim sb As New List(Of String) + sb.Add(Microsoft.VisualBasic.ChrW(34) & [Enum].GetName(GetType(DATEV_Kennzeichen), Me.Kennzeichen) & Microsoft.VisualBasic.ChrW(34)) + sb.Add(Me.Versionsnummer.ToString) + sb.Add(CInt(Me.Formatkategorie).ToString) + sb.Add(Microsoft.VisualBasic.ChrW(34) & Me.Formatname & Microsoft.VisualBasic.ChrW(34)) + sb.Add(CInt(Me.Formatversion).ToString) + sb.Add(created.ToString("yyyyMMddHHmmssfff")) + sb.Add("") '7 Leerfeld + sb.Add(EmptyDblQuote) '8 Leerfeld + sb.Add(EmptyDblQuote) '9 Leerfeld + sb.Add(EmptyDblQuote) '10 Leerfeld + sb.Add(Me.Beraternummer.ToString) '11 Beraternummer + sb.Add(Me.Mandantennummer.ToString) '12 Mandantennummer + sb.Add(Me.WJBeginn.ToString("yyyyMMdd")) + sb.Add(Me.Sachkontenlänge.ToString) + sb.Add(Me.Datum_von.ToString("yyyyMMdd")) + sb.Add(Me.Datum_bis.ToString("yyyyMMdd")) + sb.Add(Microsoft.VisualBasic.ChrW(34) & Me.Bezeichnung & Microsoft.VisualBasic.ChrW(34)) + sb.Add(EmptyDblQuote) '18 Diktatkürzel + sb.Add(CInt(Me.Buchungstyp).ToString) + sb.Add(CInt(Me.Rechnungsslegungszweck).ToString) + sb.Add(If(Me.Festschreibung = False, 0, 1)) + sb.Add(Microsoft.VisualBasic.ChrW(34) & Me.WKZ & Microsoft.VisualBasic.ChrW(34)) + sb.Add("") '23 Leerfeld + sb.Add("") '24 Leerfeld + sb.Add("") '25 Leerfeld + sb.Add("") '26 Leerfeld + sb.Add("") '27 Sachkontenrahmen + sb.Add("") '28 ID der Branchenlösung + sb.Add("") '29 Leerfeld + sb.Add("") '30 Leerfeld + sb.Add("") '31 Verarbeitungskennzeichen + Return vbs.Join(sb.ToArray, ";") + End Get + End Property + + + End Class + + + + Public Enum DATEV_Rechnungslegungszweck + unabhängig = 0 + Steuerrecht = 30 + Kalkulatorik = 40 + Handelsrecht = 50 + IFRS = 64 + End Enum + + Public Enum DATEV_Buchungstyp + Finanzbuchführung = 1 + Jahresabschluss = 2 + End Enum + + Public Enum DATEV_Kennzeichen + EXTF + DTVF + End Enum + + Public Enum DATEV_Formatkategorie + [Debitoren__Kreditoren] = 16 + [Sachkontenbeschriftungen] = 20 + [Buchungsstapel] = 21 + [Zahlungsbedingungen] = 46 + [Diverse_Adressen] = 48 + [Wiederkehrende_Buchungen] = 65 + End Enum + Public Enum DATEV_Formatversion + [Debitoren__Kreditoren] = 5 + [Sachkontenbeschriftungen] = 3 + [Buchungsstapel_9] = 9 + [Buchungsstapel] = 12 + [Zahlungsbedingungen] = 2 + [Wiederkehrende_Buchungen] = 4 + [Diverse_Adressen] = 2 + End Enum + + Public Class DATEV_Document + Public guid As String + Public type As Integer = 2 + Public processID As Integer = 2 + Public Filename As String + Public KeyWords As String + Public Repository As String + Public Register As String + Public Sub New(guid As String, filename As String, keywords As String, Repository As String, Register As String) + Me.guid = guid + Me.Filename = filename + Me.KeyWords = keywords + Me.Repository = Repository + Me.Register = Register + End Sub + End Class + + + Public Function getDatevZip(ByRef stream As IO.Stream, ByVal tgtdate As Date, mode As String, ByVal AuthUser As String, ByVal includeFiles As Boolean, Optional DebugDetails As Boolean = False) As IO.FileInfo + + Using mfr As New fds_MFR_Client + Try + Dim DSet As SQLDataSet = System.Threading.Tasks.Task.Run(Async Function() Await getSQLDataSet_async("EXECUTE [dbo].[fds__getDatevExports] @tgtdate, @mode, @files, @authuser;", FDSConnectionString, SqlParameterList:=New List(Of SqlClient.SqlParameter) From {SQL_date("@tgtdate", tgtdate), SQL_VarChar("@mode", mode), SQL_bit("@files", includeFiles), SQL_VarChar("@authuser", AuthUser)}, tablenames:=New String() {"admin", "inv", "buchungen", "debitoren"}, options:=New fds_SQLOptions())).Result() + + Dim bedi_files As New List(Of DATEV_Document) + + If DSet.Count >= 4 Then + Dim admin As Dictionary(Of String, Object) = DSet.Tables("admin").Rows(0).toObjectDictionary() + Dim startdate As Date = admin("startdate"), enddate As Date = admin("enddate") + Dim Datev_Level3_Register As String = tgtdate.ToString("yyyy\/MM") & If(mode.ToLower = "w", "_w" & tgtdate.ToString("dd"), "") + Dim fls As New Dictionary(Of String, Byte()) + Dim header As New DATEV_HEADER With { + .Formatkategorie = DATEV_Formatkategorie.Buchungsstapel, + .Formatversion = DATEV_Formatversion.Buchungsstapel_9, + .Beraternummer = admin("beraternummer"), + .Mandantennummer = admin("mandantennummer"), + .WJBeginn = admin("WJ-Beginn"), + .Sachkontenlänge = admin("Sachkontenlänge"), + .Datum_von = startdate, + .Datum_bis = enddate, + .Bezeichnung = "fds_" & mode & tgtdate.ToString("yyMMdd") + } + fls.Add("EXTF_PCW_Buchungen_0_" & startdate.ToString("yyMMdd HHmmss") & ".csv", DATEV(header, DSet.Tables("buchungen")).ToByteArray(encoding:=Encoding.GetEncoding("ISO-8859-1"))) + + ''only for debug + 'Using fsw As New IO.StreamWriter("C:\Users\sailo\Desktop\Fuchs\DatevUpload_AR 202107_w___DATEVNEU\EXTF_PCW_Buchungen_0_" & startdate.ToString("yyMMdd HHmmss") & ".csv", False, Encoding.GetEncoding("ISO-8859-1")) + ' fsw.Write(value:=DATEV(header, DSet.Tables("buchungen"))) + ' fsw.Close() + 'End Using + + + With header + .Formatkategorie = DATEV_Formatkategorie.Debitoren__Kreditoren + .Formatversion = DATEV_Formatversion.Debitoren__Kreditoren + End With + fls.Add("EXTF_PCW_Debitoren_0_" & startdate.ToString("yyMMdd HHmmss") & ".csv", DATEV(header, DSet.Tables("debitoren")).ToByteArray(encoding:=Encoding.GetEncoding("ISO-8859-1"))) + + + ''only for debug + 'Using fsw As New IO.StreamWriter("C:\Users\sailo\Desktop\Fuchs\DatevUpload_AR 202107_w___DATEVNEU\EXTF_PCW_Debitoren_0_" & startdate.ToString("yyMMdd HHmmss") & ".csv", False, Encoding.GetEncoding("ISO-8859-1")) + ' fsw.Write(value:=DATEV(header, DSet.Tables("debitoren"))) + ' fsw.Close() + 'End Using + + + 'add csv files to bedi list + For Each fl As String In fls.Keys + bedi_files.Add(New DATEV_Document(Guid.NewGuid().ToString.ToLower, filename:=fl, keywords:="", Repository:="Processweb_Daten", Register:=Datev_Level3_Register)) + Next + + If includeFiles = True Then + For Each irow As DataRow In DSet.Tables("inv").Rows + Dim fl As Byte() = Nothing, fln As String = irow.nz("DocumentName") + Try + If fln <> "" AndAlso irow.no("fds", True) = False Then + fl = mfr.GetFile(irow.nz("URI")) + Else + fl = irow.no("file", Nothing) + End If + Catch ex As Exception + + End Try + If IsNothing(fl) = False AndAlso fln <> "" Then + fls.Add(fln, fl) + bedi_files.Add(New DATEV_Document(irow("file_guid"), filename:=fln, keywords:="RgNr: " & irow.nz("InvoiceId") & " RgDatum: " & irow.ndt_string("DateOfCreation", "dd.MM.yyyy"), Repository:="ProcessWeb_Belege", Register:=Datev_Level3_Register)) + End If + 'If fls.Count > 7 Then Exit For + Next + + fls.Add("document.xml", createDATEV_document_xml(bedi_files).ToByteArray()) + End If + + If fls.Count > 0 Then + Dim ArchiveFile As New IO.FileInfo(IO.Path.GetTempPath & "DatevUpload_AR " & startdate.ToString("yyyyMM") & "_" & admin("mode").ToString & ".zip") + Try + Using Archive As New fds.Archive(ArchiveFile, Type:=SevenZip.OutArchiveFormat.Zip) + If Archive.CompressToStream(fls, targetstream:=stream) = True Then + Diagnostics.Debug.Print("ok") + 'Archive.WriteArchiveStreamToDisk() + stream.Position = 0 + Return ArchiveFile + Else + Return Nothing + End If + End Using + Catch aex As Exception + End Try + + End If + + End If + + Catch ex As Exception + Call DebugLog("getDatevZip - mfr", exc:=ex) + If DebugDetails = True Then Call DebugToFile("getDatevZip - mfr", exc:=ex, data:="", filename:="DebugDetail.txt") + End Try + End Using + Return Nothing + End Function +#Enable Warning BC42358 + + Public Function createDATEV_document_xml(files As List(Of DATEV_Document)) As String + Dim xmldoc As New Xml.XmlDocument + + 'Write down the XML declaration + Dim XmlDeclaration As Xml.XmlDeclaration = xmldoc.CreateXmlDeclaration("1.0", "UTF-8", Nothing) + xmldoc.InsertBefore(XmlDeclaration, xmldoc.DocumentElement) + + 'create root + Dim RootNode As Xml.XmlElement = xmldoc.CreateElement("archive", "http://xml.datev.de/bedi/tps/document/v04.0") + xmldoc.AppendChild(RootNode) + + With RootNode + Dim schemaLocation As Xml.XmlAttribute = xmldoc.CreateAttribute("xsi", "schemaLocation", "http://www.w3.org/2001/XMLSchema-instance") + schemaLocation.Value = "http://xml.datev.de/bedi/tps/document/v04.0 document_v040.xsd" + .Attributes.Append(schemaLocation) + .SetAttribute("generatingSystem", "ProcessWeb") + .SetAttribute("version", "4.0") + End With + + Dim header As Xml.XmlElement = xmldoc.CreateElement("header") + xmldoc.DocumentElement.PrependChild(header) + Dim headerelement As Xml.XmlElement = xmldoc.CreateElement("date") + headerelement.InnerText = Now().ToString("yyyy-MM-ddTHH:mm:ss") + header.AppendChild(headerelement) + headerelement = xmldoc.CreateElement("description") + headerelement.InnerText = "DATEV Schnittstelle ProcessWeb" + header.AppendChild(headerelement) + + Dim content As Xml.XmlElement = xmldoc.CreateElement("content") + xmldoc.DocumentElement.AppendChild(content) + For Each fl As DATEV_Document In files + Dim docelement As Xml.XmlElement = xmldoc.CreateElement("document") + docelement.SetAttribute("guid", fl.guid) + docelement.SetAttribute("type", fl.type) + docelement.SetAttribute("processID", fl.processID) + + If If(fl.KeyWords, "") <> "" Then + Dim keyword As Xml.XmlElement = xmldoc.CreateElement("keywords") + keyword.InnerText = fl.KeyWords + docelement.AppendChild(keyword) + End If + + Dim extension As Xml.XmlElement = xmldoc.CreateElement("extension") + Dim ext_attr As Xml.XmlAttribute = xmldoc.CreateAttribute("xsi", "type", "http://www.w3.org/2001/XMLSchema-instance") + ext_attr.Value = "File" + extension.Attributes.Append(ext_attr) + extension.SetAttribute("name", fl.Filename) + docelement.AppendChild(extension) + + Dim repository As Xml.XmlElement = xmldoc.CreateElement("repository") + Dim level As Xml.XmlElement + level = xmldoc.CreateElement("level") + level.SetAttribute("id", "1") 'Kategorie + level.SetAttribute("name", "ProcessWeb") + repository.AppendChild(level) + level = xmldoc.CreateElement("level") + level.SetAttribute("id", "2") 'Ordner + level.SetAttribute("name", fl.Repository) + repository.AppendChild(level) + level = xmldoc.CreateElement("level") + level.SetAttribute("id", "3") 'Register e.g. 2020/12 + level.SetAttribute("name", fl.Register) + repository.AppendChild(level) + docelement.AppendChild(repository) + + content.AppendChild(docelement) + Next + + Dim xmlasstring As String = "" + Using sw As New IO.StringWriter + Using tw As Xml.XmlWriter = Xml.XmlWriter.Create(sw) + xmldoc.WriteTo(tw) + tw.Flush() + xmlasstring = sw.GetStringBuilder().ToString() + End Using + End Using + Return xmlasstring + End Function + + + Public Function UpdateNeedValue(need As String) As UpdateNeed + Return [Enum].Parse(GetType(UpdateNeed), need) + End Function + + + + End Module + + + + + + Public Class fds_MFR_Client + Implements IDisposable + + + Dim MFRClient As Global.MFR_RESTClient.MFRClient + + Private Shared Function DefaultCredentials() + Return New MFRClientCredentials(My.Settings.MFR_UserName, My.Settings.MFR_Password) + End Function + + Public Sub New(Credentials As MFRClientCredentials) + Dim config As New MFRClientConfig(My.Settings.MFR_host) + Me.MFRClient = New MFRClient(config, credentials:=Credentials) + End Sub + Public Sub New() + Me.New(fds_MFR_Client.DefaultCredentials()) + End Sub + + Public Property IsReadonly() As Boolean + Get + Return Me.MFRClient.IsReadonly + End Get + Set(value As Boolean) + Me.MFRClient.IsReadonly = value + End Set + End Property + + Public ReadOnly Property clientConfig As MFRClientConfig + Get + Return Me.MFRClient.clientConfig + End Get + End Property + + Public Async Function ReadAnything(address As String, Optional throwerror_if_nOK As Boolean = True) As Task(Of String) + Return Await Me.MFRClient.ReadAnything(address:=address, throwerror_if_nOK:=throwerror_if_nOK) + End Function + Public Function GetFile(address As String, Optional throwerror_if_nOK As Boolean = True) As Byte() + Return Me.MFRClient.GetFile(address:=address, throwerror_if_nOK:=throwerror_if_nOK) + End Function + Public Async Function ReadOData(address As String, Optional throwerror_if_nOK As Boolean = True) As Task(Of ODataEnvelope) + Return Await Me.MFRClient.ReadOData(address:=address, throwerror_if_nOK:=throwerror_if_nOK) + End Function + Public Async Function getEntities(Optional throwerror_if_nOK As Boolean = True) As Task(Of String) + Return Await Me.MFRClient.getEntities(throwerror_if_nOK:=throwerror_if_nOK) + End Function + + Public Class DatabaseSchema + Private _et As EntityTypes + + Public ReadOnly Property isValid As Boolean = False + Public ReadOnly Property hasEntity As Boolean = False + + Public ReadOnly Property ThisEntityName As String + Get + Return EntityName(Me._et) + End Get + End Property + + Public ReadOnly Property EntityConfig As Dictionary(Of String, String) + Public ReadOnly Property Entitytablename As String + Public ReadOnly Property Complextypes As New Dictionary(Of String, Dictionary(Of String, String)) + Public ReadOnly Property NavProperties As New Dictionary(Of String, Dictionary(Of String, String)) + + Private _tableset As DataSet + + + + Public Sub New(et As EntityTypes) + Me._et = et + Dim DSet As SQLDataSet = System.Threading.Tasks.Task.Run(Async Function() + Return Await getSQLDataSet_async("EXECUTE [dbo].[mfr__getSchema] 'table', @tgttype;", FDSConnectionString, SqlParameterList:=New List(Of SqlClient.SqlParameter) From {New SqlClient.SqlParameter("@tgttype", ThisEntityName)}, tablenames:=New String() {"entity", "complex_types", "navigation_properties", "tables"}, options:=New fds_SQLOptions()) + End Function).Result() + + Me._isValid = DSet.Count > 0 + Me._hasEntity = DSet.Tables("entity").Rows.Count = 1 + + Me._EntityConfig = If(DSet.Tables("entity").Rows.Count > 0, DSet.Tables("entity").Rows(0).toStringDictionary(), New Dictionary(Of String, String)) + Me._Entitytablename = If(Me.EntityConfig.ContainsKey("tablename"), Me.EntityConfig("tablename"), "") + + For Each ctrw As DataRow In DSet.Tables("complex_types").Rows() + Me._Complextypes.Add(ctrw.nz("name").ToLower, ctrw.toStringDictionary()) + Next + For Each nprw As DataRow In DSet.Tables("navigation_properties").Rows() + Me._NavProperties.Add(nprw.nz("name").ToLower, nprw.toStringDictionary()) + Next + Dim sql As New List(Of String) From {newDatatable_SQL(Me.Entitytablename)} 'The target entitytype's table MUST be the first + Dim tablenames As New List(Of String) From {Me.Entitytablename} + + For Each ttrw As DataRow In DSet.Tables("tables").Rows + sql.Add(newDatatable_SQL(ttrw.nz("tablename"))) + tablenames.Add(ttrw.nz("tablename")) + Next + Me._tableset = System.Threading.Tasks.Task.Run(Async Function() + Return Await getSQLDataSet_async(vbs.Join(sql.ToArray(), vbNewLine), FDSConnectionString, tablenames:=tablenames.ToArray, options:=New fds_SQLOptions()) + End Function).Result().DataSet + End Sub + + Public Function tgtDataset(SetID As String) As DataSet + Dim dset As New DataSet + For Each tbl As DataTable In Me._tableset.Tables + Dim t As DataTable = tbl.Clone + If t.Columns.Contains("setid") Then t.Columns("setid").DefaultValue = SetID + dset.Tables.Add(t) + Next + Return dset + End Function + + End Class + + +#Disable Warning BC42356 ' This async method lacks 'Await' operators and so will run synchronously + Public Async Function Update__entitytable(et As EntityTypes, UpdateNeed As fds.fds_mfr.UpdateNeed, EntityID As Long(), Optional DebugDetails As Boolean = False, Optional SchemaDic As Dictionary(Of String, DatabaseSchema) = Nothing, Optional AdditionalCommandAfter As String = "") As Threading.Tasks.Task(Of Boolean) + If et = EntityTypes.none Then Return False + EntityID = If(EntityID, New Long() {}) ''make sure is not nothing + If UpdateNeed = UpdateNeed.None AndAlso If(EntityID, New Long() {}).Length = 0 Then Return True + + Dim StartTime As DateTime = Now() + Diagnostics.Debug.Print(StartTime.ToLongTimeString() & " - " & "Start before Schema " & et.ToString) + Dim SetID As String = RandomString(5) 'generate a unique ID to identify the submitted data and prevent mixup. A table can be accessed by several batch processes in parallel. + Dim ThisEntityName As String = EntityName(et) + + + Dim DtF As New Action(Of String, String, String)(Sub(note As String, info As String, data As String) + If DebugDetails = True Then Call DebugToFile("Update__entitytable - " & note & " For " & ThisEntityName & "(" & SetID & ")" & If(If(info, "") <> "", ": " & info, ""), filename:="DebugDetail.txt") + End Sub) + Dim Dlg As New Action(Of String, String, String)(Sub(note As String, info As String, data As String) + Dim str As String = "Update__entitytable - " & note & " for " & ThisEntityName & "(" & SetID & ")" & If(If(info, "") <> "", ": " & info, "") + Call DebugLog(str, data:=If(data, "")) + If DebugDetails = True Then Call DebugToFile(str, filename:="DebugDetail.txt") + Diagnostics.Debug.Print(str) + End Sub) + + + + 'check if update processes not already in progress and set lock + Dim params As New List(Of SqlClient.SqlParameter) From { + New SqlClient.SqlParameter("@tbl", ThisEntityName), + New SqlClient.SqlParameter("@state", True), + New SqlClient.SqlParameter("@override", False), + New SqlClient.SqlParameter("@setid", SetID) + } + Dim lockstate As Object = True 'getSQLValue("EXECUTE [dbo].[ctm__admin_getTableLock] @tbl, @state, @override, @setid;", con, SqlParameterList:=params, options:=New fds_SQLOptions()) + If DebugDetails = True AndAlso lockstate.GetType() = GetType(System.Boolean) Then Call DebugToFile("Update__entitytable - lock received for " & ThisEntityName & "(" & SetID & ") - " & lockstate.ToString, filename:="DebugDetail.txt") + 'true means lock was successful + If lockstate.GetType() = GetType(System.Boolean) AndAlso DirectCast(lockstate, Boolean) = True Then + Try + 'get Schema + Dim Schema As DatabaseSchema = If(IsNothing(SchemaDic) = False AndAlso SchemaDic.ContainsKey(ThisEntityName), SchemaDic(ThisEntityName), New DatabaseSchema(et)) + If Schema.isValid = False Then + Dlg("Schema not found", "", Nothing) + ElseIf Schema.hasEntity = True Then + Dim tgtDataset As DataSet = Schema.tgtDataset(SetID:=SetID) + + Dim lastDate As SQLObject, Filter As New List(Of String), EntityIDFilter As String = "", isProcessed As Boolean = False + If If(EntityID, New Long() {}).Length = 1 Then + EntityIDFilter = "(" & EntityID(0).ToString() & "L)" + Filter.Add("") + UpdateNeed = UpdateNeed.Reset + ElseIf If(EntityID, New Long() {}).Length > 1 Then + EntityIDFilter = "" + For idx As Integer = 0 To EntityID.Length - 1 Step 50 + Filter.Add("$filter=" & EntityID.Skip(idx).Take(50).Convert(Of String)(Function(l As Long) "Id eq " & l.ToString() & "L").Join(" or ")) + Next + UpdateNeed = UpdateNeed.Reset + ElseIf UpdateNeed > UpdateNeed.Short Then + EntityIDFilter = "" + Filter.Add("") + ElseIf Schema.EntityConfig.ContainsKey("DateColumn") AndAlso If(Schema.EntityConfig("DateColumn"), "") <> "" Then + lastDate = Await getSQLValue_async(Schema.EntityConfig.nz("DateSQL").ne("SELECT MAX([" & Schema.EntityConfig("DateColumn") & "]) FROM [dbo].[" & Schema.Entitytablename & "];"), FDSConnectionString, options:=New fds_SQLOptions()) + Filter.Add(If(UpdateNeed = fds.fds_mfr.UpdateNeed.Short AndAlso IsNothing(lastDate.Result) = False AndAlso lastDate.Result.GetType() = GetType(System.DateTime), "$filter=" & Schema.EntityConfig("DateColumn") & " gt DateTime'" & DirectCast(lastDate.Result, System.DateTime).ToString(format:="yyyy\-MM\-dd") & "T00:00:00'", "")) + End If + For Each iFilter As String In Filter + If (Schema.EntityConfig.ContainsKey("url") AndAlso Schema.EntityConfig("url") <> "") AndAlso (UpdateNeed = UpdateNeed.Short AndAlso iFilter <> "") OrElse (UpdateNeed > UpdateNeed.Short) Then + isProcessed = True + Diagnostics.Debug.Print(Now.ToLongTimeString() & " - " & "Start MFR " & Schema.Entitytablename) + Dim MFR_Response As ODataEnvelope = Await ReadOData(Me.MFRClient.clientConfig.BaseUrl & Schema.EntityConfig("url").Replace("?", EntityIDFilter & "?") & If(iFilter <> "" AndAlso Schema.EntityConfig("url").Contains("?") = False, "?", If(iFilter <> "", "&", "")) & iFilter) + Diagnostics.Debug.Print(Now.ToLongTimeString() & " - " & "End MFR " & Schema.Entitytablename) + Do Until IsNothing(MFR_Response) = True + If MFR_Response.value.GetType() = GetType(JObject) Then + MFR_Response.ConvertToArray() + End If + Try + + If MFR_Response.value.GetType() = GetType(JArray) AndAlso IsNothing(DirectCast(MFR_Response.value, Newtonsoft.Json.Linq.JContainer).First) = False AndAlso DirectCast(DirectCast(DirectCast(MFR_Response.value, Newtonsoft.Json.Linq.JContainer).First, Newtonsoft.Json.Linq.JContainer).First, Newtonsoft.Json.Linq.JProperty).Name = "odata.error" AndAlso DirectCast(MFR_Response.value, Newtonsoft.Json.Linq.JContainer).First.ToString().IndexOf("Sequence contains no elements", 0, StringComparison.InvariantCultureIgnoreCase) > -1 Then + MFR_Response.value = New JArray() + End If + Catch jbex As Exception + End Try + If MFR_Response.value.GetType() = GetType(JArray) Then + Diagnostics.Debug.Print(Now.ToLongTimeString() & " - " & DirectCast(MFR_Response.value, JArray).Count & ":" & tgtDataset.Tables(Schema.Entitytablename).Rows.Count & "- this " & Strings.Right(MFR_Response.url, 10)) + For Each vi As JObject In MFR_Response.value + Dim vdic As Dictionary(Of String, Object) = vi.ToObject(Of Dictionary(Of String, Object)) + Dim etrow As DataRow = tgtDataset.Tables(Schema.Entitytablename).NewRow + etrow.Item("setid") = SetID + etrow.Item("Id") = vdic("Id") + For Each vkey As String In vdic.Keys + If IsNothing(vdic(vkey)) = False Then + If Schema.Complextypes.ContainsKey(vkey.ToLower) AndAlso Schema.Complextypes(vkey.ToLower)("name") <> "CustomValues" Then + Call StoreCP(tgtDataset, etrow, Schema.Complextypes, CP:=vdic(vkey), EntityProperty:=ThisEntityName & ":" & vkey, vkey:=vkey, setid:=SetID, vdic:=vdic) + ElseIf Schema.Complextypes.ContainsKey(vkey.ToLower) AndAlso Schema.Complextypes(vkey.ToLower)("name") = "CustomValues" Then + If etrow.Table.Columns.Contains(vkey) Then etrow.Item(vkey) = json.SerializeObject(vdic(vkey)) + ElseIf Schema.NavProperties.ContainsKey(vkey.ToLower) Then + Call StoreNP(tgtDataset, Schema.Complextypes, Schema.NavProperties(vkey.ToLower), NP:=vdic(vkey), EntityProperty:=ThisEntityName & ":" & vkey, setid:=SetID, vdic:=vdic) + Else + If etrow.Table.Columns.Contains(vkey) Then + If vdic(vkey).GetType() = GetType(System.DateTime) Then + If Year(vdic(vkey)) > 1900 Then etrow.Item(vkey) = vdic(vkey) + ElseIf vdic(vkey).GetType = etrow.Table.Columns(vkey).DataType Then + etrow.Item(vkey) = vdic(vkey) + ElseIf vdic(vkey).GetType = GetType(String) And vdic(vkey).GetType() <> etrow.Table.Columns(vkey).DataType Then + Try + etrow.Item(vkey) = DirectCast(vdic(vkey), System.String).TryConvert(etrow.Table.Columns(vkey).DataType) + Catch cgvex As Exception + Try + etrow.Item(vkey) = vdic(vkey) + Catch cvex As Exception + Diagnostics.Debug.Print(Now.ToLongTimeString() & " - " & "cvex inner " & cvex.Message) + End Try + End Try + Else + Try + etrow.Item(vkey) = vdic(vkey) + Catch cvex As Exception + Diagnostics.Debug.Print(Now.ToLongTimeString() & " - " & "cvex outer " & cvex.Message) + End Try + End If + End If + End If + End If + Next + tgtDataset.Tables(Schema.Entitytablename).Rows.Add(etrow) + tgtDataset.Tables(Schema.Entitytablename).AcceptChanges() + Next + Else + Diagnostics.Debug.Print(Now.ToLongTimeString() & " - " & " MFR_Response not jArray") + End If + If IsNothing(MFR_Response.nextlink) = False AndAlso MFR_Response.nextlink.ToString <> "" Then + MFR_Response = Await ReadOData(MFR_Response.nextlink.ToString) + Else + MFR_Response = Nothing + End If + + Loop + End If + Next iFilter + + Diagnostics.Debug.Print(Now.ToLongTimeString() & " - " & "Start SQL " & Schema.Entitytablename) + If isProcessed = True Then + For Each tbl As DataTable In tgtDataset.Tables + Dim trc As Integer = tbl.Rows.Count + If trc > 0 OrElse If(EntityID, New Long() {}).Length > 0 Then + Try + tbl.AcceptChanges() + Dim dtwa As New DatatableWriterAsync(tbl, FDSConnectionString) + dtwa.CommandAfterError = New SqlClient.SqlCommand("SELECT * INTO [t_" & SetID.ToLower & "_" & tbl.TableName & "] FROM " & dtwa.DestinationTableName & ";") + + dtwa.CommandAfter = New SqlClient.SqlCommand('"SELECT * INTO [t_" & SetID.ToLower & "_" & tbl.TableName & "] FROM " & dtwa.DestinationTableName & ";" & + "EXECUTE [dbo].[" & (tbl.TableName).Replace("__", "__updt__") & "] @tblname, @referencetable, @tgtid;" & + "EXECUTE [dbo].[fds__setStatus] @table,@action,@setid,@info;" & If(AdditionalCommandAfter, "")) + dtwa.CommandAfter.Parameters.AddRange(New SqlClient.SqlParameter() { + New SqlClient.SqlParameter("@tblname", dtwa.DestinationTableName), + New SqlClient.SqlParameter("@table", tbl.TableName), + New SqlClient.SqlParameter("@action", If(tbl.TableName.ToLower = Schema.Entitytablename.ToLower, "update_", "imdu_") & UpdateNeed.ToString().ToLower()), + New SqlClient.SqlParameter("@setid", SetID.ToLower), + New SqlClient.SqlParameter("@entityname", Schema.ThisEntityName), + New SqlClient.SqlParameter("@info", json.SerializeObject(New With {.count = trc, .reference = Schema.Entitytablename, .id = If(If(EntityID, New Long() {}).Length > 0, vbs.Join(EntityID.Convert(Of String)(Function(x As Long) x.ToString).ToArray, ","), Nothing)})), + New SqlClient.SqlParameter("@referencetable", Schema.Entitytablename), + New SqlClient.SqlParameter("@tgtid", If(If(EntityID, New Long() {}).Length > 0, EntityID(0).ToString, DBNull.Value)) + }) + Diagnostics.Debug.Print("tbs:" & tbl.TableName & " - " & dtwa.DestinationTableName & " - " & SetID.ToLower) + dtwa.DoSubmit() + Catch dsex As Exception + Dlg("Submission issue", dsex.Message, "table: " & tbl.TableName) + End Try + Diagnostics.Debug.Print("OK") + End If + Next + Diagnostics.Debug.Print(Now.ToLongTimeString() & " - " & "End SQL " & Schema.Entitytablename) + End If + End If + Catch ex As Exception + Dlg(ex.Message, "", ex.StackTrace) + Finally + 'release lock + params = New List(Of SqlClient.SqlParameter) From { + New SqlClient.SqlParameter("@tbl", ThisEntityName), + New SqlClient.SqlParameter("@state", False), + New SqlClient.SqlParameter("@override", False), + New SqlClient.SqlParameter("@setid", SetID) + } + Dim Exceptionmessages_gtl As String = "" + 'setSQLValue("EXECUTE [dbo].[ctm__admin_getTableLock] @tbl, @state, @override, @setid;", connectionstring:=FDSConnectionString, SqlParameterList:=params, ExceptionMessage:=Exceptionmessages_gtl) + If DebugDetails = True Then Call DebugToFile("Update__entitytable - locked released for " & ThisEntityName & "(" & SetID & ")", filename:="DebugDetail.txt") + End Try + Else + 'abort action if locked = true + Debug.Print("Updates already locked for " & ThisEntityName & "(" & SetID & ")") + If DebugDetails = True Then Call DebugToFile("Update__entitytable - Updates already locked For " & ThisEntityName & "(" & SetID & ")", filename:="DebugDetail.txt") + End If + Return True + End Function + + + + Private Function StoreCP(ByRef TgtDataset As DataSet, ByRef etrow As DataRow, Complextypes As Dictionary(Of String, Dictionary(Of String, String)), ByVal CP As Object, ByVal EntityProperty As String, ByVal vkey As String, ByVal setid As String, ByVal vdic As Dictionary(Of String, Object)) As Boolean + If CP.GetType = GetType(JArray) Then + For Each vitm As JObject In vdic(vkey) + Call StoreCP(TgtDataset, etrow, Complextypes, CP:=vitm, EntityProperty:=EntityProperty, vkey:=vkey, setid:=setid, vdic:=vdic) + Next + ElseIf CP.GetType = GetType(JObject) Then + Dim CxType As Dictionary(Of String, String) = Complextypes(vkey.ToLower) + Dim cto As Dictionary(Of String, Object) = CP.ToObject(Of Dictionary(Of String, Object)) + Dim cnr As DataRow = TgtDataset.Tables(CxType("tablename")).NewRow + cnr.Item("setid") = setid + For Each cky As String In cto.Keys + If IsNothing(cto(cky)) = False AndAlso cnr.Table.Columns.Contains(cky) Then + 'cnr.Item(cky) = cto(cky) + If cto(cky).GetType() = GetType(System.DateTime) Then + If Year(cto(cky)) > 1900 Then cnr.Item(cky) = cto(cky) + ElseIf cto(cky).GetType = cnr.Table.Columns(cky).DataType Then + cnr.Item(cky) = cto(cky) + ElseIf cto(cky).GetType = GetType(String) And cto(cky).GetType() <> cnr.Table.Columns(cky).DataType Then + Try + cnr.Item(cky) = DirectCast(cto(cky), System.String).TryConvert(cnr.Table.Columns(cky).DataType) + Catch cgvex As Exception + Try + cnr.Item(cky) = cto(cky) + Catch cvex As Exception + End Try + End Try + Else + Try + cnr.Item(cky) = cto(cky) + Catch cvex As Exception + End Try + End If + End If + Next + If cnr.Table.Columns.Contains("EntityId") Then cnr.Item("EntityId") = vdic("Id") + If cnr.Table.Columns.Contains("Property") Then cnr.Item("Property") = EntityProperty + TgtDataset.Tables(CxType("tablename")).Rows.Add(cnr) + If etrow.Table.Columns.Contains(vkey & "#ID") AndAlso cto.ContainsKey("id") Then etrow.Item(vkey & "#ID") = vdic("Id") + End If + + Return True + End Function + + Private Function StoreNP(ByRef TgtDataset As DataSet, Complextypes As Dictionary(Of String, Dictionary(Of String, String)), NavProp As Dictionary(Of String, String), ByVal NP As Object, ByVal EntityProperty As String, ByVal setid As String, ByVal vdic As Dictionary(Of String, Object)) As Boolean + If NP.GetType() = GetType(JArray) Then + For Each vitm As JObject In NP + Call StoreNP(TgtDataset:=TgtDataset, Complextypes:=Complextypes, NavProp:=NavProp, NP:=vitm, EntityProperty:=EntityProperty, setid:=setid, vdic:=vdic) + Next + ElseIf NP.GetType() = GetType(JObject) Then + Dim nto As Dictionary(Of String, Object) = NP.ToObject(Of Dictionary(Of String, Object)) + If nto.ContainsKey("Id") Then + Dim nnr As DataRow = TgtDataset.Tables(NavProp("countertable")).NewRow + Dim ncr As DataRow = TgtDataset.Tables(NavProp("tablename")).NewRow + nnr.Item("setid") = setid + For Each cky As String In nto.Keys + If IsNothing(nto(cky)) = False Then + If Complextypes.ContainsKey(cky.ToLower) AndAlso Complextypes(cky.ToLower)("name") <> "CustomValues" Then + Call StoreCP(TgtDataset:=TgtDataset, etrow:=nnr, Complextypes:=Complextypes, CP:=nto(cky), EntityProperty:=Complextypes(cky.ToLower)("EntityType") & ":" & cky, vkey:=cky, setid:=setid, vdic:=vdic) + Else + If IsNothing(nto(cky)) = False AndAlso nnr.Table.Columns.Contains(cky) Then + 'nnr.Item(cky) = nto(cky) + If nto(cky).GetType() = GetType(System.DateTime) Then + If Year(nto(cky)) > 1900 Then nnr.Item(cky) = nto(cky) + ElseIf nto(cky).GetType = nnr.Table.Columns(cky).DataType Then + nnr.Item(cky) = nto(cky) + ElseIf nto(cky).GetType = GetType(String) And nto(cky).GetType() <> nnr.Table.Columns(cky).DataType Then + Try + nnr.Item(cky) = DirectCast(nto(cky), System.String).TryConvert(nnr.Table.Columns(cky).DataType) + Catch cgvex As Exception + Try + nnr.Item(cky) = nto(cky) + Catch cvex As Exception + End Try + End Try + Else + Try + nnr.Item(cky) = nto(cky) + Catch cvex As Exception + End Try + End If + End If + End If + End If + Next + ncr.Item("EntityId") = vdic("Id") + ncr.Item("PartnerType") = NavProp("countertype") + ncr.Item("PartnerId") = nto("Id") + ncr.Item("Property") = EntityProperty + TgtDataset.Tables(NavProp("tablename")).Rows.Add(ncr) + TgtDataset.Tables(NavProp("countertable")).Rows.Add(nnr) + End If + End If + Return True + End Function + + Private Shared Function newDatatable_SQL(tablename As String) As String + Return System.String.Format("Select TOP(0) [setid] = CAST('' as varchar(50)), * FROM [dbo].[{0}];", tablename) + End Function + Private Async Function newDatatable(tablename As String, setid As String) As Task(Of DataTable) + Dim sqldt As SQLDataTable = Await getSQLDatatable_async(newDatatable_SQL(tablename:=tablename), SqlConnectionString:=FDSConnectionString, options:=New fds_SQLOptions()) + Dim dt As DataTable = sqldt.DataTable.Clone + dt.TableName = tablename + If dt.Columns.Contains("setid") = False Then dt.Columns.Add(New DataColumn("setid", GetType(System.String)) With {.MaxLength = 5}) + dt.Columns("setid").DefaultValue = setid + Return dt + End Function +#Enable Warning BC42356 ' This async method lacks 'Await' operators and so will run synchronously + + Public Async Function Update__Entitytables(Optional DebugDetails As Boolean = False, Optional tgtEntityType As Nullable(Of EntityTypes) = Nothing) As Threading.Tasks.Task + Dim UpdateTasks As New List(Of Threading.Tasks.Task) + + Dim DtF As New Action(Of String, String, String, Exception)(Sub(note As String, info As String, data As String, ex As Exception) + If DebugDetails = True Then Call DebugToFile("Update__AllEntitytables - " & note & If(If(info, "") <> "", ": " & info, ""), filename:="DebugDetail.txt", exc:=ex, data:=data) + End Sub) + Dim Dlg As New Action(Of String, String, String, Exception)(Sub(note As String, info As String, data As String, ex As Exception) + Dim str As String = "Update__AllEntitytables - " & note & If(If(info, "") <> "", ": " & info, "") + Call DebugLog(str, data:=If(data, ""), exc:=ex) + If DebugDetails = True Then Call DebugToFile(str, filename:="DebugDetail.txt", exc:=ex, data:=data) + Debug.Print(str) + End Sub) + + Try + 'get updateable tables + 'the SQL query returns all tables that have corresponding temp tables and update procedures + 'a lock status is included to determine if update processes are running + Dim UpdateableTables As SQLDataTable = Await getSQLDatatable_async("SELECT * FROM [dbo].[fds__getUpdateableTables]()" & If(tgtEntityType.HasValue, " WHERE [entity_name] = '" & tgtEntityType.Value.ToString & "'", "") & ";", FDSConnectionString, options:=New fds_SQLOptions()) + Call DtF("UpdateableTables ", UpdateableTables.Exception, "(" & If(UpdateableTables.Count > 0, UpdateableTables.DataTable.Rows.Count.ToString, " no") & " Rows)", Nothing) + + If UpdateableTables.Count > 0 AndAlso UpdateableTables.DataTable.Columns.Contains("updateneed") = True Then 'just to avoid exceptions in next steps + For Each UpdateableTable_rw As DataRow In UpdateableTables.Select("updateneed > 0", "updateneed DESC") + Dim etname As String = UpdateableTable_rw.nz("entity_name", "") + Try 'create capsule for each cycle to prevent everything fails if anything fails + If etname = "" Then + Call DtF("no entity_name received", "", json.SerializeObject(UpdateableTable_rw), Nothing) + ElseIf UpdateableTable_rw.nbool("locked", True) = False Then + Dim Tbl_UpdateNeed As UpdateNeed = UpdateableTable_rw.nint("updateneed", 0) + Dim et As generic.EntityTypes = _generic.EntityValue(etname) + If Not et = EntityTypes.none Then + Call DtF("updating: " & etname, "", json.SerializeObject(UpdateableTable_rw), Nothing) + + Await Update__entitytable(et, UpdateNeed:=Tbl_UpdateNeed, EntityID:=Nothing, DebugDetails:=DebugDetails) + Call DtF("updating task completed", "", "", Nothing) + Else + Call DtF("entitytype not known", "", json.SerializeObject(New With {.etname = etname}), Nothing) + End If + Else + Call DtF("Table update locked" & etname, "", json.SerializeObject(UpdateableTable_rw), Nothing) + End If + Catch tableupdate_ex As Exception + Call Dlg("updatepart failed - " & etname, "", json.SerializeObject(UpdateableTable_rw), tableupdate_ex) + End Try + Next UpdateableTable_rw + End If + Catch exa As Exception + Call Dlg("outer frame", "", "", Nothing) + End Try + End Function + Public Async Function Update__EntityRequests(Optional DebugDetails As Boolean = False) As Threading.Tasks.Task + Dim UpdateTasks As New List(Of Threading.Tasks.Task) + + Dim DtF As New Action(Of String, String, String, Exception)(Sub(note As String, info As String, data As String, ex As Exception) + If DebugDetails = True Then Call DebugToFile("Update__EntityRequests - " & note & If(If(info, "") <> "", ": " & info, ""), filename:="DebugDetail.txt", exc:=ex, data:=data) + End Sub) + Dim Dlg As New Action(Of String, String, String, Exception)(Sub(note As String, info As String, data As String, ex As Exception) + Dim str As String = "Update__EntityRequests - " & note & If(If(info, "") <> "", ": " & info, "") + Call DebugLog(str, data:=If(data, ""), exc:=ex) + If DebugDetails = True Then Call DebugToFile(str, filename:="DebugDetail.txt", exc:=ex, data:=data) + Debug.Print(str) + End Sub) + + Try + 'get updateable tables + 'the SQL query returns all tables that have corresponding temp tables and update procedures + 'a lock status is included to determine if update processes are running + Dim UpdateableRequests As SQLDataTable = Await getSQLDatatable_async("SELECT * FROM [dbo].[fds__getUpdateableRequests]();", FDSConnectionString, options:=New fds_SQLOptions()) + Call DtF("UpdateableRequests ", UpdateableRequests.Exception, "(" & If(UpdateableRequests.Count > 0, UpdateableRequests.DataTable.Rows.Count.ToString, " no") & " Rows)", Nothing) + + If UpdateableRequests.Count > 0 Then 'just to avoid exceptions in next steps + For Each UpdateableRequest_rw As DataRow In UpdateableRequests.Select("", "order") + Dim etname As String = UpdateableRequest_rw.nz("entity_name", "") + Dim tgtid As Long = UpdateableRequest_rw.nint("Id", -1) + If tgtid > -1 AndAlso etname.IsNullOrWhiteSpace = False Then + Try 'create capsule for each cycle to prevent everything fails if anything fails + Dim et As generic.EntityTypes = _generic.EntityValue(etname) + Await Update__entitytable(et, UpdateNeed:=UpdateNeed.Reset, EntityID:=New Long() {tgtid}, DebugDetails:=DebugDetails, AdditionalCommandAfter:="DELETE FROM [dbo].[fds__mfr_updaterequests] WHERE LOWER([entity_name]) = LOWER(ISNULL(@entityname,'')) AND CAST(ISNULL([Id],'') as varchar(1000)) = @tgtid;") + + Catch tableupdate_ex As Exception + Call Dlg("Update__EntityRequest single failed - " & etname, "", json.SerializeObject(UpdateableRequest_rw), tableupdate_ex) + End Try + End If + Next UpdateableRequest_rw + End If + Catch exa As Exception + Call Dlg("outer frame", "", "", Nothing) + End Try + End Function + + Public Enum PostSubmissionActionMode + full + incremental + reset + End Enum + + + + Public Sub Dispose() Implements IDisposable.Dispose + ''Me.MFRClient.Logout() + DirectCast(Me.MFRClient, IDisposable).Dispose() + End Sub + + + + + End Class + +End Namespace \ No newline at end of file diff --git a/DataService_Legacy/fds_shared.vb b/DataService_Legacy/fds_shared.vb new file mode 100644 index 0000000..c12d49e --- /dev/null +++ b/DataService_Legacy/fds_shared.vb @@ -0,0 +1,226 @@ + + +Friend Module fds_shared + + Friend Function SQLConnectionString() As String + Return Configuration.ConfigurationManager.ConnectionStrings("fuchs_ConnectionString").ConnectionString + End Function + Friend Function FDSConnectionString() As String + Return Configuration.ConfigurationManager.ConnectionStrings("fuchs_fds_ConnectionString").ConnectionString + End Function + Friend Function SqlCon() As SqlClient.SqlConnection + Return New SqlClient.SqlConnection(Configuration.ConfigurationManager.ConnectionStrings("fuchs_ConnectionString").ConnectionString) + End Function + + + + Public Function RandomString(rs_length As Byte) As String + Dim r As New Random() + Dim s As String = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + Dim sb As New Text.StringBuilder + For i As Byte = 1 To rs_length + Dim idx As Integer = r.Next(0, s.Length) + sb.Append(s.Substring(idx, 1)) + Next + Return sb.ToString() + End Function + + + ''''+
+
+ - Rechnungseditor
+ o Opt. Textfeld oberhalb der Tabelle
+ o Opt. Textfeld unterhalb der Tabelle
+ o Textbausteine
+ - Eigene Rechnung hochladen (nicht autom/online erzeugt)
+ § Anlegen eines Datensatzes (zur Nummernvergabe)
+ § Upload des pdf-Dokuments
+ - Rechnungen
+ o UTC -> local
+ o Rechnungs-Nummernkreis für FDS ( ab R2021-1196 )
+ o Storno-Rechnung
+ § „Freigabe“ des Auftrags/der Aufträge
+ · wahlweise ohne Freigabe der Aufträge
+ § Storno als neue Rechnung mit neg. Betrag
+ § Storno als Kopie der stornierten Rechnung mit Berücksichtigung des stornierten Betrages
+ o Fortsetzen von Rechnungsentwürfen
+ o Löschen von Rechnungsentwürfen
+ o Abschlags-Rechnungen
+ § Abschlagsrechnungen erstellen
+ § „Offen lassen“ von Aufträgen
+ § Berücksichtigung von Abschlagsrechnungen in abschließende Rechnungen
+ § Angepasste Texte für A.Rechnungen
+ § Nummerierte Abschlagsrechnungen je „Hauptauftrag“
+ § Anzeige der Abschlagsrechnungen unterhalb aller Aufträge und Zwischensummen
+ o Zahlungs-Status anzeigen
+ o Zahlungs-Summe anzeigen (eingegangene Beträge)
+ o Zugeordnete Buchungen anzeigen
+ o Manuelles Abschließen (Bezahlt markieren = ist bezahlt)
+ o Manuelles Abschließen (Bezahlt markieren = ist nicht (vollst.) bezahlt)
+ o Seitenzahlen in das Layout
+ o GoDB-Konforme elektronische Rechnungen
+ o ZUGFeRD Meta-Daten für E-Invoicing
+ o GiroCode (EPC-QR-Code mit Rechnungsdaten; Banking Apps können das lesen)
+ § Auf der Rechnung
+ § Auf der Mahnung (nur offener Betrag)
+ § In den Emails
+ o Emails individualisiert
+ o Hinweis wenn keine Email-Adresse
+ o Post-Versand über „Emailbrief.de“ (Die pdfs werden direkt über einen Online-Dienst postalisch versandt)
+ o Änderungen Rechnung 09.06.2021
+ - Auftragsname veränderbar
+ - Anstatt Auftragsnamen -> Ausgeführte Arbeiten aus der Checkliste
+ - Positionsnummern aktualisieren bei gelöschten Zeilen, wenn da gewesen.
+ - Keine Zwischensumme wenn nur ein Auftrag
+ - Default UsT
+ - USt pro Auftrag „zusammen“ verändern
+ - Finanzamtshinweis „Als Privathaushalt ….. von dieser Rechnung
+ - Rechnungsnummer = nicht auswählbar in der Liste
+ - Datum des Auftrags bei jedem Auftrag „01.01.21: “
+ - Formular : Anzahl verändern -> Gesamtbetrag verändern
+ - Doppelung der Hinweistexte
+ - Zeitraum
+ o Auftragsliste
+ - Kunden in Liste
+ - Kunde in Detail-Ansicht
+ - Einsatzorte korrigieren
+ -
+ - Datev-Export:
+ o Rechnungskopie im Zip (für nicht-MFR-Rechnungen (fds))
+ - Download von Rechnungen
+ o Aktualisierung des Button in der Rechnungsliste für fds-Dokumente
+ - Übersicht je Kunde
+ o Rechnungen
+ o Aufträge
+ o Kontobewegungen
+ - Unterschiedliche Rechte für Rechnungen mit und ohne Preisanpassungen
+ - Verarbeitung von Kontobewegungen
+ o Zuordnungsliste (alle Buchungen mit autom. + manuellen Zuordnungen)
+ o Manuelle Zuordnung
+ o Manuelle Bestätigung erforderlich
+ o Autom. Zuordnung zu Rechnungen (Abgleich mit Kunden- und Rechnungsinfos)
+ o Überlauf (Eingänge nicht zuordenbar)
+ o Zuordnung löschen
+ o Anbindung ein Banking-API System (bspw https://banksapi.de/api/) zur autom. Abfrage von Buchungen
+ - Mahnungs-Modul
+ o Mahnungsvorschläge
+ o Mahnsperre ?
+ o Mahnungserstellung
+ § 3 Mahn-Stufen
+ § Online-Editor
+ § Autom. Versand
+ § Postversand
+ o Unterschiedliche Texte/Vorlagen für unterschiedliche Kunden-Typen
+ - Tageszusammenfassungen
+ o Neue fertige Aufträge (für Rechnungen)
+ o Neue überfällige Rechnungen (für Mahnungen)
+ o Auffällige Kontobuchungen
+ - Adressbuch
+ o Matching zu MFR-Kunden (CustomerID)
+ o Kontaktpersonen
+ o „Liefer“-Adressen
+ o Administrative Standardadressen (Post und Email) für best. Zwecke (z.B. Rechnungen, Mahnungen, Ansprechpartner)
+ o Auswahlmöglichkeit im Rechnungs- und Mahnungs-Editor
+ - Email-Log
+ - Sicherheit
+ o Verschlüsselung der Datenbank
+ o Modul-Autorisierungen
+ § Unterschiedliche Rechte für Rechnungen mit und ohne Preisanpassungen
+ o S/MIME-Signatur der ausgehenden Emails
+
+
Herzliche Grüße aus Düsseldorf-Bilk
Ihr Team der Firma Sebastian Fuchs