Add backend-authoritative invoice draft editing (ADR 0006/0007) #1
Binary file not shown.
@@ -1,131 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<configuration>
|
|
||||||
<configSections>
|
|
||||||
<sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
|
||||||
<section name="fds.My.MySettings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
|
|
||||||
</sectionGroup>
|
|
||||||
</configSections>
|
|
||||||
|
|
||||||
<connectionStrings>
|
|
||||||
<add name="fuchs_ConnectionString" connectionString="Data Source=MSSQL4.NBG4.DOMAINXYZ.DE,10439;Initial Catalog=site_fuchs;Persist Security Info=False;TrustServerCertificate=true;Encrypt=true;User ID=fuchs_web;password='Bt5pL/cJg9oxb5';Connect Timeout=60;Load Balance Timeout=240;Max Pool Size=500;" providerName="System.Data.SqlClient" />
|
|
||||||
<add name="fuchs_fds_ConnectionString" connectionString="Data Source=MSSQL4.NBG4.DOMAINXYZ.DE,10439;Initial Catalog=site_fuchs;Persist Security Info=False;TrustServerCertificate=true;Encrypt=true;User ID=fuchs_fds;password='!Po@cGZ5bUn37khO';Connect Timeout=60;Load Balance Timeout=240;Max Pool Size=500;" providerName="System.Data.SqlClient" />
|
|
||||||
</connectionStrings>
|
|
||||||
<startup>
|
|
||||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
|
||||||
</startup>
|
|
||||||
<applicationSettings>
|
|
||||||
<fds.My.MySettings>
|
|
||||||
<setting name="ExecutionFrequency_Minutes" serializeAs="String">
|
|
||||||
<value>15</value>
|
|
||||||
</setting>
|
|
||||||
<setting name="DebugDetails" serializeAs="String">
|
|
||||||
<value>True</value>
|
|
||||||
</setting>
|
|
||||||
<setting name="MFR_UserName" serializeAs="String">
|
|
||||||
<value>system@sebastian-fuchs---bad-und-heizung-gmbh-und-co-kg.com</value>
|
|
||||||
</setting>
|
|
||||||
<setting name="MFR_Password" serializeAs="String">
|
|
||||||
<value>0oT4G3H2</value>
|
|
||||||
</setting>
|
|
||||||
<setting name="MFR_host" serializeAs="String">
|
|
||||||
<value>portal.mobilefieldreport.com</value>
|
|
||||||
</setting>
|
|
||||||
</fds.My.MySettings>
|
|
||||||
</applicationSettings>
|
|
||||||
<runtime>
|
|
||||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.Diagnostics.DiagnosticSource" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-7.0.0.2" newVersion="7.0.0.2" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.Net.Http.Formatting" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-5.2.7.0" newVersion="5.2.7.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="Microsoft.Azure.Services.AppAuthentication" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-1.6.2.0" newVersion="1.6.2.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.IdentityModel.Tokens.Jwt" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-7.0.2.0" newVersion="7.0.2.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="Microsoft.IdentityModel.Clients.ActiveDirectory" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-5.3.0.0" newVersion="5.3.0.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.Numerics.Vectors" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-4.1.4.0" newVersion="4.1.4.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.ValueTuple" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="Microsoft.IdentityModel.Tokens" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-7.0.2.0" newVersion="7.0.2.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="Microsoft.IdentityModel.Logging" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-6.11.0.0" newVersion="6.11.0.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="Microsoft.IdentityModel.JsonWebTokens" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-6.11.0.0" newVersion="6.11.0.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="Microsoft.IO.RecyclableMemoryStream" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-2.3.2.0" newVersion="2.3.2.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="BouncyCastle.Crypto" publicKeyToken="0e99375e54769942" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-1.9.0.0" newVersion="1.9.0.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="MimeKit" publicKeyToken="bede1c8a46c66814" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-3.0.0.0" newVersion="3.0.0.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.Text.Json" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-7.0.0.3" newVersion="7.0.0.3" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="Microsoft.Bcl.AsyncInterfaces" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-7.0.0.0" newVersion="7.0.0.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.Text.Encoding.CodePages" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="Microsoft.Web.Infrastructure" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-2.0.0.0" newVersion="2.0.0.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-4.0.1.2" newVersion="4.0.1.2" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.ComponentModel.Annotations" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-4.2.1.0" newVersion="4.2.1.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity name="System.Text.Encodings.Web" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
|
||||||
<bindingRedirect oldVersion="0.0.0.0-7.0.0.0" newVersion="7.0.0.0" />
|
|
||||||
</dependentAssembly>
|
|
||||||
</assemblyBinding>
|
|
||||||
</runtime>
|
|
||||||
</configuration>
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
||||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
|
||||||
<PropertyGroup>
|
|
||||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
|
||||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
|
||||||
<ProjectGuid>{7A56E271-A6BE-4C34-A859-DADEBC4C7A54}</ProjectGuid>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<StartupObject>Sub Main</StartupObject>
|
|
||||||
<RootNamespace>fds</RootNamespace>
|
|
||||||
<AssemblyName>Fuchs_DataService</AssemblyName>
|
|
||||||
<FileAlignment>512</FileAlignment>
|
|
||||||
<MyType>Console</MyType>
|
|
||||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
|
||||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
|
||||||
<Deterministic>true</Deterministic>
|
|
||||||
<IsWebBootstrapper>false</IsWebBootstrapper>
|
|
||||||
<PublishUrl>publish\</PublishUrl>
|
|
||||||
<Install>true</Install>
|
|
||||||
<InstallFrom>Disk</InstallFrom>
|
|
||||||
<UpdateEnabled>false</UpdateEnabled>
|
|
||||||
<UpdateMode>Foreground</UpdateMode>
|
|
||||||
<UpdateInterval>7</UpdateInterval>
|
|
||||||
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
|
|
||||||
<UpdatePeriodically>false</UpdatePeriodically>
|
|
||||||
<UpdateRequired>false</UpdateRequired>
|
|
||||||
<MapFileExtensions>true</MapFileExtensions>
|
|
||||||
<ApplicationRevision>0</ApplicationRevision>
|
|
||||||
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
|
|
||||||
<UseApplicationTrust>false</UseApplicationTrust>
|
|
||||||
<BootstrapperEnabled>true</BootstrapperEnabled>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
|
||||||
<PlatformTarget>x64</PlatformTarget>
|
|
||||||
<DebugSymbols>true</DebugSymbols>
|
|
||||||
<DebugType>full</DebugType>
|
|
||||||
<DefineDebug>true</DefineDebug>
|
|
||||||
<DefineTrace>true</DefineTrace>
|
|
||||||
<OutputPath>bin\Debug\</OutputPath>
|
|
||||||
<DocumentationFile>Fuchs_DataService.xml</DocumentationFile>
|
|
||||||
<NoWarn>42016,41999,42017,42018,42019,42032,42036,42020,42021,42022</NoWarn>
|
|
||||||
<Prefer32Bit>false</Prefer32Bit>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
|
||||||
<DebugType>pdbonly</DebugType>
|
|
||||||
<DefineDebug>false</DefineDebug>
|
|
||||||
<DefineTrace>true</DefineTrace>
|
|
||||||
<Optimize>true</Optimize>
|
|
||||||
<OutputPath>bin\Release\</OutputPath>
|
|
||||||
<DocumentationFile>Fuchs_DataService.xml</DocumentationFile>
|
|
||||||
<NoWarn>42016,41999,42017,42018,42019,42032,42036,42020,42021,42022</NoWarn>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup>
|
|
||||||
<OptionExplicit>On</OptionExplicit>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup>
|
|
||||||
<OptionCompare>Binary</OptionCompare>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup>
|
|
||||||
<OptionStrict>Off</OptionStrict>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup>
|
|
||||||
<OptionInfer>On</OptionInfer>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Reference Include="Microsoft.Web.Infrastructure, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\..\..\NugetPackages\Microsoft.Web.Infrastructure.2.0.0\lib\net40\Microsoft.Web.Infrastructure.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\..\..\NugetPackages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="SevenZipSharp, Version=1.6.1.23, Culture=neutral, PublicKeyToken=c8ff6ba0184838bb, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\..\..\NugetPackages\Squid-Box.SevenZipSharp.1.6.1.23\lib\net472\SevenZipSharp.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System" />
|
|
||||||
<Reference Include="System.Configuration" />
|
|
||||||
<Reference Include="System.Configuration.Install" />
|
|
||||||
<Reference Include="System.Data" />
|
|
||||||
<Reference Include="System.Deployment" />
|
|
||||||
<Reference Include="System.Runtime.InteropServices.RuntimeInformation, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\..\..\NugetPackages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll</HintPath>
|
|
||||||
<Private>True</Private>
|
|
||||||
<Private>True</Private>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.ServiceProcess" />
|
|
||||||
<Reference Include="System.Web" />
|
|
||||||
<Reference Include="System.Web.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\..\..\NugetPackages\Microsoft.AspNet.Razor.3.2.9\lib\net45\System.Web.Razor.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="System.Xml" />
|
|
||||||
<Reference Include="System.Core" />
|
|
||||||
<Reference Include="System.Xml.Linq" />
|
|
||||||
<Reference Include="System.Data.DataSetExtensions" />
|
|
||||||
<Reference Include="System.Net.Http" />
|
|
||||||
<Reference Include="Topshelf, Version=4.3.0.0, Culture=neutral, PublicKeyToken=b800c4cfcdeea87b, processorArchitecture=MSIL">
|
|
||||||
<HintPath>..\..\..\NugetPackages\Topshelf.4.3.0\lib\net452\Topshelf.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Import Include="Microsoft.VisualBasic" />
|
|
||||||
<Import Include="System" />
|
|
||||||
<Import Include="System.Collections" />
|
|
||||||
<Import Include="System.Collections.Generic" />
|
|
||||||
<Import Include="System.Data" />
|
|
||||||
<Import Include="System.Diagnostics" />
|
|
||||||
<Import Include="System.Linq" />
|
|
||||||
<Import Include="System.Xml.Linq" />
|
|
||||||
<Import Include="System.Threading.Tasks" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Include="fds_zip.vb" />
|
|
||||||
<Compile Include="fds_debug.vb" />
|
|
||||||
<Compile Include="fds_mfr.vb" />
|
|
||||||
<Compile Include="fds_shared.vb" />
|
|
||||||
<Compile Include="My Project\AssemblyInfo.vb" />
|
|
||||||
<Compile Include="My Project\Application.Designer.vb">
|
|
||||||
<AutoGen>True</AutoGen>
|
|
||||||
<DependentUpon>Application.myapp</DependentUpon>
|
|
||||||
<DesignTime>True</DesignTime>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="My Project\Resources.Designer.vb">
|
|
||||||
<AutoGen>True</AutoGen>
|
|
||||||
<DesignTime>True</DesignTime>
|
|
||||||
<DependentUpon>Resources.resx</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="My Project\Settings.Designer.vb">
|
|
||||||
<AutoGen>True</AutoGen>
|
|
||||||
<DependentUpon>Settings.settings</DependentUpon>
|
|
||||||
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="fds_main.vb" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<EmbeddedResource Include="My Project\Resources.resx">
|
|
||||||
<Generator>VbMyResourcesResXFileCodeGenerator</Generator>
|
|
||||||
<LastGenOutput>Resources.Designer.vb</LastGenOutput>
|
|
||||||
<CustomToolNamespace>My.Resources</CustomToolNamespace>
|
|
||||||
<SubType>Designer</SubType>
|
|
||||||
</EmbeddedResource>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Content Include="install.bat" />
|
|
||||||
<None Include="My Project\Application.myapp">
|
|
||||||
<Generator>MyApplicationCodeGenerator</Generator>
|
|
||||||
<LastGenOutput>Application.Designer.vb</LastGenOutput>
|
|
||||||
</None>
|
|
||||||
<None Include="My Project\Settings.settings">
|
|
||||||
<Generator>SettingsSingleFileGenerator</Generator>
|
|
||||||
<CustomToolNamespace>My</CustomToolNamespace>
|
|
||||||
<LastGenOutput>Settings.Designer.vb</LastGenOutput>
|
|
||||||
</None>
|
|
||||||
<None Include="App.config" />
|
|
||||||
<None Include="packages.config" />
|
|
||||||
<Content Include="un-install.bat" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\..\WebProjectComponents\OCMS\OCMS.vbproj">
|
|
||||||
<Project>{ac8cba60-d786-48fd-a9f0-8b045a7bd505}</Project>
|
|
||||||
<Name>OCMS</Name>
|
|
||||||
</ProjectReference>
|
|
||||||
<ProjectReference Include="..\MFR_RESTClient\MFR_RESTClient.vbproj">
|
|
||||||
<Project>{00c70b53-516d-4d56-ad25-6757094b4335}</Project>
|
|
||||||
<Name>MFR_RESTClient</Name>
|
|
||||||
</ProjectReference>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Content Include="7z.dll">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<BootstrapperPackage Include=".NETFramework,Version=v4.8">
|
|
||||||
<Visible>False</Visible>
|
|
||||||
<ProductName>Microsoft .NET Framework 4.8 %28x86 and x64%29</ProductName>
|
|
||||||
<Install>true</Install>
|
|
||||||
</BootstrapperPackage>
|
|
||||||
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
|
|
||||||
<Visible>False</Visible>
|
|
||||||
<ProductName>.NET Framework 3.5 SP1</ProductName>
|
|
||||||
<Install>false</Install>
|
|
||||||
</BootstrapperPackage>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup />
|
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.VisualBasic.targets" />
|
|
||||||
</Project>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
'------------------------------------------------------------------------------
|
|
||||||
' <auto-generated>
|
|
||||||
' This code was generated by a tool.
|
|
||||||
' Runtime Version:4.0.30319.42000
|
|
||||||
'
|
|
||||||
' Changes to this file may cause incorrect behavior and will be lost if
|
|
||||||
' the code is regenerated.
|
|
||||||
' </auto-generated>
|
|
||||||
'------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
Option Strict On
|
|
||||||
Option Explicit On
|
|
||||||
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<MyApplicationData xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
|
||||||
<MySubMain>false</MySubMain>
|
|
||||||
<SingleInstance>false</SingleInstance>
|
|
||||||
<ShutdownMode>0</ShutdownMode>
|
|
||||||
<EnableVisualStyles>true</EnableVisualStyles>
|
|
||||||
<AuthenticationMode>0</AuthenticationMode>
|
|
||||||
<ApplicationType>2</ApplicationType>
|
|
||||||
<SaveMySettingsOnExit>true</SaveMySettingsOnExit>
|
|
||||||
</MyApplicationData>
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
Imports System
|
|
||||||
Imports System.Reflection
|
|
||||||
Imports System.Runtime.InteropServices
|
|
||||||
|
|
||||||
' General Information about an assembly is controlled through the following
|
|
||||||
' set of attributes. Change these attribute values to modify the information
|
|
||||||
' associated with an assembly.
|
|
||||||
|
|
||||||
' Review the values of the assembly attributes
|
|
||||||
|
|
||||||
<Assembly: AssemblyTitle("Fuchs_DataService")>
|
|
||||||
<Assembly: AssemblyDescription("")>
|
|
||||||
<Assembly: AssemblyCompany("")>
|
|
||||||
<Assembly: AssemblyProduct("Fuchs_DataService")>
|
|
||||||
<Assembly: AssemblyCopyright("Copyright © 2021")>
|
|
||||||
<Assembly: AssemblyTrademark("")>
|
|
||||||
|
|
||||||
<Assembly: ComVisible(False)>
|
|
||||||
|
|
||||||
'The following GUID is for the ID of the typelib if this project is exposed to COM
|
|
||||||
<Assembly: Guid("b4650e09-34ae-4c0f-b973-63439b8a22f0")>
|
|
||||||
|
|
||||||
' Version information for an assembly consists of the following four values:
|
|
||||||
'
|
|
||||||
' Major Version
|
|
||||||
' Minor Version
|
|
||||||
' Build Number
|
|
||||||
' Revision
|
|
||||||
'
|
|
||||||
' You can specify all the values or you can default the Build and Revision Numbers
|
|
||||||
' by using the '*' as shown below:
|
|
||||||
' <Assembly: AssemblyVersion("1.0.*")>
|
|
||||||
|
|
||||||
<Assembly: AssemblyVersion("1.0.0.0")>
|
|
||||||
<Assembly: AssemblyFileVersion("1.0.0.0")>
|
|
||||||
-63
@@ -1,63 +0,0 @@
|
|||||||
'------------------------------------------------------------------------------
|
|
||||||
' <auto-generated>
|
|
||||||
' This code was generated by a tool.
|
|
||||||
' Runtime Version:4.0.30319.42000
|
|
||||||
'
|
|
||||||
' Changes to this file may cause incorrect behavior and will be lost if
|
|
||||||
' the code is regenerated.
|
|
||||||
' </auto-generated>
|
|
||||||
'------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
Option Strict On
|
|
||||||
Option Explicit On
|
|
||||||
|
|
||||||
Imports System
|
|
||||||
|
|
||||||
Namespace My.Resources
|
|
||||||
|
|
||||||
'This class was auto-generated by the StronglyTypedResourceBuilder
|
|
||||||
'class via a tool like ResGen or Visual Studio.
|
|
||||||
'To add or remove a member, edit your .ResX file then rerun ResGen
|
|
||||||
'with the /str option, or rebuild your VS project.
|
|
||||||
'''<summary>
|
|
||||||
''' A strongly-typed resource class, for looking up localized strings, etc.
|
|
||||||
'''</summary>
|
|
||||||
<Global.System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0"), _
|
|
||||||
Global.System.Diagnostics.DebuggerNonUserCodeAttribute(), _
|
|
||||||
Global.System.Runtime.CompilerServices.CompilerGeneratedAttribute(), _
|
|
||||||
Global.Microsoft.VisualBasic.HideModuleNameAttribute()> _
|
|
||||||
Friend Module Resources
|
|
||||||
|
|
||||||
Private resourceMan As Global.System.Resources.ResourceManager
|
|
||||||
|
|
||||||
Private resourceCulture As Global.System.Globalization.CultureInfo
|
|
||||||
|
|
||||||
'''<summary>
|
|
||||||
''' Returns the cached ResourceManager instance used by this class.
|
|
||||||
'''</summary>
|
|
||||||
<Global.System.ComponentModel.EditorBrowsableAttribute(Global.System.ComponentModel.EditorBrowsableState.Advanced)> _
|
|
||||||
Friend ReadOnly Property ResourceManager() As Global.System.Resources.ResourceManager
|
|
||||||
Get
|
|
||||||
If Object.ReferenceEquals(resourceMan, Nothing) Then
|
|
||||||
Dim temp As Global.System.Resources.ResourceManager = New Global.System.Resources.ResourceManager("fds.Resources", GetType(Resources).Assembly)
|
|
||||||
resourceMan = temp
|
|
||||||
End If
|
|
||||||
Return resourceMan
|
|
||||||
End Get
|
|
||||||
End Property
|
|
||||||
|
|
||||||
'''<summary>
|
|
||||||
''' Overrides the current thread's CurrentUICulture property for all
|
|
||||||
''' resource lookups using this strongly typed resource class.
|
|
||||||
'''</summary>
|
|
||||||
<Global.System.ComponentModel.EditorBrowsableAttribute(Global.System.ComponentModel.EditorBrowsableState.Advanced)> _
|
|
||||||
Friend Property Culture() As Global.System.Globalization.CultureInfo
|
|
||||||
Get
|
|
||||||
Return resourceCulture
|
|
||||||
End Get
|
|
||||||
Set
|
|
||||||
resourceCulture = value
|
|
||||||
End Set
|
|
||||||
End Property
|
|
||||||
End Module
|
|
||||||
End Namespace
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<root>
|
|
||||||
<!--
|
|
||||||
Microsoft ResX Schema
|
|
||||||
|
|
||||||
Version 2.0
|
|
||||||
|
|
||||||
The primary goals of this format is to allow a simple XML format
|
|
||||||
that is mostly human readable. The generation and parsing of the
|
|
||||||
various data types are done through the TypeConverter classes
|
|
||||||
associated with the data types.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
... ado.net/XML headers & schema ...
|
|
||||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
|
||||||
<resheader name="version">2.0</resheader>
|
|
||||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
|
||||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
|
||||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
|
||||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
|
||||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
|
||||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
|
||||||
</data>
|
|
||||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
|
||||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
|
||||||
<comment>This is a comment</comment>
|
|
||||||
</data>
|
|
||||||
|
|
||||||
There are any number of "resheader" rows that contain simple
|
|
||||||
name/value pairs.
|
|
||||||
|
|
||||||
Each data row contains a name, and value. The row also contains a
|
|
||||||
type or mimetype. Type corresponds to a .NET class that support
|
|
||||||
text/value conversion through the TypeConverter architecture.
|
|
||||||
Classes that don't support this are serialized and stored with the
|
|
||||||
mimetype set.
|
|
||||||
|
|
||||||
The mimetype is used for serialized objects, and tells the
|
|
||||||
ResXResourceReader how to depersist the object. This is currently not
|
|
||||||
extensible. For a given mimetype the value must be set accordingly:
|
|
||||||
|
|
||||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
|
||||||
that the ResXResourceWriter will generate, however the reader can
|
|
||||||
read any of the formats listed below.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.binary.base64
|
|
||||||
value : The object must be serialized with
|
|
||||||
: System.Serialization.Formatters.Binary.BinaryFormatter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.soap.base64
|
|
||||||
value : The object must be serialized with
|
|
||||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
|
||||||
value : The object must be serialized into a byte array
|
|
||||||
: using a System.ComponentModel.TypeConverter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
-->
|
|
||||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
|
||||||
<xsd:element name="root" msdata:IsDataSet="true">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:choice maxOccurs="unbounded">
|
|
||||||
<xsd:element name="metadata">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="assembly">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:attribute name="alias" type="xsd:string" />
|
|
||||||
<xsd:attribute name="name" type="xsd:string" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="data">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="resheader">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:choice>
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:schema>
|
|
||||||
<resheader name="resmimetype">
|
|
||||||
<value>text/microsoft-resx</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="version">
|
|
||||||
<value>2.0</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="reader">
|
|
||||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="writer">
|
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
</root>
|
|
||||||
-118
@@ -1,118 +0,0 @@
|
|||||||
'------------------------------------------------------------------------------
|
|
||||||
' <auto-generated>
|
|
||||||
' This code was generated by a tool.
|
|
||||||
' Runtime Version:4.0.30319.42000
|
|
||||||
'
|
|
||||||
' Changes to this file may cause incorrect behavior and will be lost if
|
|
||||||
' the code is regenerated.
|
|
||||||
' </auto-generated>
|
|
||||||
'------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
Option Strict On
|
|
||||||
Option Explicit On
|
|
||||||
|
|
||||||
|
|
||||||
Namespace My
|
|
||||||
|
|
||||||
<Global.System.Runtime.CompilerServices.CompilerGeneratedAttribute(), _
|
|
||||||
Global.System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.8.1.0"), _
|
|
||||||
Global.System.ComponentModel.EditorBrowsableAttribute(Global.System.ComponentModel.EditorBrowsableState.Advanced)> _
|
|
||||||
Partial Friend NotInheritable Class MySettings
|
|
||||||
Inherits Global.System.Configuration.ApplicationSettingsBase
|
|
||||||
|
|
||||||
Private Shared defaultInstance As MySettings = CType(Global.System.Configuration.ApplicationSettingsBase.Synchronized(New MySettings()),MySettings)
|
|
||||||
|
|
||||||
#Region "My.Settings Auto-Save Functionality"
|
|
||||||
#If _MyType = "WindowsForms" Then
|
|
||||||
Private Shared addedHandler As Boolean
|
|
||||||
|
|
||||||
Private Shared addedHandlerLockObject As New Object
|
|
||||||
|
|
||||||
<Global.System.Diagnostics.DebuggerNonUserCodeAttribute(), Global.System.ComponentModel.EditorBrowsableAttribute(Global.System.ComponentModel.EditorBrowsableState.Advanced)> _
|
|
||||||
Private Shared Sub AutoSaveSettings(sender As Global.System.Object, e As Global.System.EventArgs)
|
|
||||||
If My.Application.SaveMySettingsOnExit Then
|
|
||||||
My.Settings.Save()
|
|
||||||
End If
|
|
||||||
End Sub
|
|
||||||
#End If
|
|
||||||
#End Region
|
|
||||||
|
|
||||||
Public Shared ReadOnly Property [Default]() As MySettings
|
|
||||||
Get
|
|
||||||
|
|
||||||
#If _MyType = "WindowsForms" Then
|
|
||||||
If Not addedHandler Then
|
|
||||||
SyncLock addedHandlerLockObject
|
|
||||||
If Not addedHandler Then
|
|
||||||
AddHandler My.Application.Shutdown, AddressOf AutoSaveSettings
|
|
||||||
addedHandler = True
|
|
||||||
End If
|
|
||||||
End SyncLock
|
|
||||||
End If
|
|
||||||
#End If
|
|
||||||
Return defaultInstance
|
|
||||||
End Get
|
|
||||||
End Property
|
|
||||||
|
|
||||||
<Global.System.Configuration.ApplicationScopedSettingAttribute(), _
|
|
||||||
Global.System.Diagnostics.DebuggerNonUserCodeAttribute(), _
|
|
||||||
Global.System.Configuration.DefaultSettingValueAttribute("15")> _
|
|
||||||
Public ReadOnly Property ExecutionFrequency_Minutes() As String
|
|
||||||
Get
|
|
||||||
Return CType(Me("ExecutionFrequency_Minutes"),String)
|
|
||||||
End Get
|
|
||||||
End Property
|
|
||||||
|
|
||||||
<Global.System.Configuration.ApplicationScopedSettingAttribute(), _
|
|
||||||
Global.System.Diagnostics.DebuggerNonUserCodeAttribute(), _
|
|
||||||
Global.System.Configuration.DefaultSettingValueAttribute("True")> _
|
|
||||||
Public ReadOnly Property DebugDetails() As Boolean
|
|
||||||
Get
|
|
||||||
Return CType(Me("DebugDetails"),Boolean)
|
|
||||||
End Get
|
|
||||||
End Property
|
|
||||||
|
|
||||||
<Global.System.Configuration.ApplicationScopedSettingAttribute(), _
|
|
||||||
Global.System.Diagnostics.DebuggerNonUserCodeAttribute(), _
|
|
||||||
Global.System.Configuration.DefaultSettingValueAttribute("system@sebastian-fuchs---bad-und-heizung-gmbh-und-co-kg.com")> _
|
|
||||||
Public ReadOnly Property MFR_UserName() As String
|
|
||||||
Get
|
|
||||||
Return CType(Me("MFR_UserName"),String)
|
|
||||||
End Get
|
|
||||||
End Property
|
|
||||||
|
|
||||||
<Global.System.Configuration.ApplicationScopedSettingAttribute(), _
|
|
||||||
Global.System.Diagnostics.DebuggerNonUserCodeAttribute(), _
|
|
||||||
Global.System.Configuration.DefaultSettingValueAttribute("0oT4G3H2")> _
|
|
||||||
Public ReadOnly Property MFR_Password() As String
|
|
||||||
Get
|
|
||||||
Return CType(Me("MFR_Password"),String)
|
|
||||||
End Get
|
|
||||||
End Property
|
|
||||||
|
|
||||||
<Global.System.Configuration.ApplicationScopedSettingAttribute(), _
|
|
||||||
Global.System.Diagnostics.DebuggerNonUserCodeAttribute(), _
|
|
||||||
Global.System.Configuration.DefaultSettingValueAttribute("portal.mobilefieldreport.com")> _
|
|
||||||
Public ReadOnly Property MFR_host() As String
|
|
||||||
Get
|
|
||||||
Return CType(Me("MFR_host"),String)
|
|
||||||
End Get
|
|
||||||
End Property
|
|
||||||
End Class
|
|
||||||
End Namespace
|
|
||||||
|
|
||||||
Namespace My
|
|
||||||
|
|
||||||
<Global.Microsoft.VisualBasic.HideModuleNameAttribute(), _
|
|
||||||
Global.System.Diagnostics.DebuggerNonUserCodeAttribute(), _
|
|
||||||
Global.System.Runtime.CompilerServices.CompilerGeneratedAttribute()> _
|
|
||||||
Friend Module MySettingsProperty
|
|
||||||
|
|
||||||
<Global.System.ComponentModel.Design.HelpKeywordAttribute("My.Settings")> _
|
|
||||||
Friend ReadOnly Property Settings() As Global.fds.My.MySettings
|
|
||||||
Get
|
|
||||||
Return Global.fds.My.MySettings.Default
|
|
||||||
End Get
|
|
||||||
End Property
|
|
||||||
End Module
|
|
||||||
End Namespace
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
|
||||||
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="My" GeneratedClassName="MySettings" UseMySettingsClassName="true">
|
|
||||||
<Profiles />
|
|
||||||
<Settings>
|
|
||||||
<Setting Name="ExecutionFrequency_Minutes" Type="System.String" Scope="Application">
|
|
||||||
<Value Profile="(Default)">15</Value>
|
|
||||||
</Setting>
|
|
||||||
<Setting Name="DebugDetails" Type="System.Boolean" Scope="Application">
|
|
||||||
<Value Profile="(Default)">True</Value>
|
|
||||||
</Setting>
|
|
||||||
<Setting Name="MFR_UserName" Type="System.String" Scope="Application">
|
|
||||||
<Value Profile="(Default)">system@sebastian-fuchs---bad-und-heizung-gmbh-und-co-kg.com</Value>
|
|
||||||
</Setting>
|
|
||||||
<Setting Name="MFR_Password" Type="System.String" Scope="Application">
|
|
||||||
<Value Profile="(Default)">0oT4G3H2</Value>
|
|
||||||
</Setting>
|
|
||||||
<Setting Name="MFR_host" Type="System.String" Scope="Application">
|
|
||||||
<Value Profile="(Default)">portal.mobilefieldreport.com</Value>
|
|
||||||
</Setting>
|
|
||||||
</Settings>
|
|
||||||
</SettingsFile>
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
Option Explicit On
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Partial Friend Module fds_debug
|
|
||||||
|
|
||||||
<Diagnostics.DebuggerStepThrough>
|
|
||||||
Public Function LogFile(FileName As String) As IO.FileInfo
|
|
||||||
Return New IO.FileInfo(AppBaseDirectory().FullName & FileName)
|
|
||||||
End Function
|
|
||||||
|
|
||||||
|
|
||||||
<Diagnostics.DebuggerStepThrough>
|
|
||||||
Public Function AppBaseDirectory() As IO.DirectoryInfo
|
|
||||||
Dim path As String = AppDomain.CurrentDomain.BaseDirectory + "tmp\"
|
|
||||||
Dim di As New IO.DirectoryInfo(path)
|
|
||||||
If di.Exists = True Then
|
|
||||||
Return di
|
|
||||||
ElseIf My.Computer.FileSystem.DirectoryExists(AppDomain.CurrentDomain.BaseDirectory) = True Then
|
|
||||||
di.Create()
|
|
||||||
Return di
|
|
||||||
Else : Return Nothing
|
|
||||||
End If
|
|
||||||
End Function
|
|
||||||
|
|
||||||
<Diagnostics.DebuggerStepThrough>
|
|
||||||
Public Sub DebugLog_async(CodeReference As String, SQLConnectionString As String, Optional exc As Exception = Nothing, Optional data As String = "", Optional context As Object = Nothing)
|
|
||||||
If CodeReference = "" OrElse SQLConnectionString = "" Then Exit Sub
|
|
||||||
Try
|
|
||||||
Threading.Tasks.Task.Run(Sub() Call DebugLog_sync(CodeReference:=CodeReference, SQLConnectionString:=SQLConnectionString, exc:=exc, data:=data, context:=context))
|
|
||||||
Catch ex As Exception
|
|
||||||
Call DebugLog_sync(CodeReference:="fds_debug DebugLog_async", SQLConnectionString:=SQLConnectionString, exc:=ex, data:="", context:=Nothing)
|
|
||||||
End Try
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
<Diagnostics.DebuggerStepThrough>
|
|
||||||
Public Sub DebugLog_sync(CodeReference As String, SQLConnectionString As String, Optional exc As Exception = Nothing, Optional data As String = "", Optional context As Object = Nothing)
|
|
||||||
If CodeReference = "" OrElse SQLConnectionString = "" Then Exit Sub
|
|
||||||
Using con As New SqlClient.SqlConnection(SQLConnectionString)
|
|
||||||
Call DebugLog(CodeReference:=CodeReference, SQLConnection:=con, exc:=exc, data:=data, context:=context)
|
|
||||||
End Using
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
|
|
||||||
<Diagnostics.DebuggerStepThrough>
|
|
||||||
Public Sub DebugLog(CodeReference As String, SQLConnection As SqlClient.SqlConnection, Optional exc As Exception = Nothing, Optional data As String = "", Optional context As Object = Nothing)
|
|
||||||
If CodeReference = "" OrElse IsNothing(SQLConnection) = True Then Exit Sub
|
|
||||||
Dim note As String = Now.ToString("yyyy.MM.dd HH:mm:ss") & " - " & CodeReference
|
|
||||||
Try
|
|
||||||
Try
|
|
||||||
If IsNothing(SQLConnection) = False Then
|
|
||||||
Dim pl As New List(Of SqlClient.SqlParameter) From {
|
|
||||||
New SqlClient.SqlParameter("@CodeReference", CodeReference),
|
|
||||||
New SqlClient.SqlParameter("@ExceptionMessage", If(IsNothing(exc), DBNull.Value, exc.Message)),
|
|
||||||
New SqlClient.SqlParameter("@StackTrace", If(IsNothing(exc), DBNull.Value, exc.StackTrace.ToString)),
|
|
||||||
New SqlClient.SqlParameter("@data", If(data, DBNull.Value))
|
|
||||||
}
|
|
||||||
Try
|
|
||||||
Dim w As Integer = 0
|
|
||||||
If SQLConnection.State = ConnectionState.Broken Then SQLConnection.Close()
|
|
||||||
If SQLConnection.State = ConnectionState.Connecting Then
|
|
||||||
w = 0
|
|
||||||
While SQLConnection.State = ConnectionState.Connecting And w < 10
|
|
||||||
System.Threading.Thread.Sleep(100)
|
|
||||||
w += 1
|
|
||||||
End While
|
|
||||||
ElseIf Not SQLConnection.State = ConnectionState.Open Then
|
|
||||||
SQLConnection.Open()
|
|
||||||
End If
|
|
||||||
w = 0
|
|
||||||
While Not SQLConnection.State = ConnectionState.Open And w < 10
|
|
||||||
System.Threading.Thread.Sleep(100)
|
|
||||||
w += 1
|
|
||||||
End While
|
|
||||||
Dim cmd As New SqlClient.SqlCommand("EXECUTE [dbo].[fds__admin_logdebug] @CodeReference,@ExceptionMessage,@StackTrace,@Data;", SQLConnection)
|
|
||||||
cmd.Parameters.AddRange(pl.ToArray)
|
|
||||||
Call cmd.ExecuteNonQuery()
|
|
||||||
'SQLConnection.Close()
|
|
||||||
cmd.Parameters.Clear()
|
|
||||||
|
|
||||||
Catch sqlex As Exception
|
|
||||||
End Try
|
|
||||||
|
|
||||||
End If
|
|
||||||
Catch dbex As Exception
|
|
||||||
|
|
||||||
End Try
|
|
||||||
|
|
||||||
If IsNothing(exc) = False Then
|
|
||||||
note &= (vbCrLf & "Exception:" & exc.Message & vbCrLf & "Stack:" & exc.StackTrace.ToString).Replace(vbLf, vbLf & " ")
|
|
||||||
End If
|
|
||||||
If data <> "" Then
|
|
||||||
note &= (vbCrLf & "Data:" & data).Replace(vbLf, vbLf & " ")
|
|
||||||
End If
|
|
||||||
note &= vbCrLf
|
|
||||||
|
|
||||||
Dim DebugLogfile As IO.FileInfo = LogFile("DebugLog.txt")
|
|
||||||
If DebugLogfile.Directory.Exists = True Then
|
|
||||||
IO.File.AppendAllText(DebugLogfile.FullName, note)
|
|
||||||
End If
|
|
||||||
Catch logex As Exception
|
|
||||||
|
|
||||||
Finally
|
|
||||||
|
|
||||||
Console.Write(note)
|
|
||||||
Debug.Print(note)
|
|
||||||
End Try
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
Public Sub DebugToFile(note As String, Optional filename As String = "DebugLog.txt")
|
|
||||||
Try
|
|
||||||
Dim DebugLogfile As IO.FileInfo = LogFile(filename)
|
|
||||||
If DebugLogfile.Directory.Exists = True Then
|
|
||||||
IO.File.AppendAllText(DebugLogfile.FullName, Now.ToString & ": " & note & vbCrLf)
|
|
||||||
End If
|
|
||||||
Catch ex As Exception
|
|
||||||
End Try
|
|
||||||
End Sub
|
|
||||||
Public Sub DebugToFile(CodeReference As String, exc As Exception, data As String, Optional filename As String = "DebugLog.txt")
|
|
||||||
Dim note As String = CodeReference
|
|
||||||
If IsNothing(exc) = False Then
|
|
||||||
note &= (vbCrLf & "Exception:" & exc.Message & vbCrLf & "Stack:" & exc.StackTrace.ToString).Replace(vbLf, vbLf & " ")
|
|
||||||
End If
|
|
||||||
If data <> "" Then
|
|
||||||
note &= (vbCrLf & "Data:" & data).Replace(vbLf, vbLf & " ")
|
|
||||||
End If
|
|
||||||
|
|
||||||
Call DebugToFile(note, filename:=filename)
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
End Module
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
|
|
||||||
Imports Topshelf
|
|
||||||
Imports json = Newtonsoft.Json.JsonConvert
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Public Class fds_service
|
|
||||||
Implements Topshelf.ServiceControl
|
|
||||||
|
|
||||||
Dim WithEvents _timer As System.Timers.Timer
|
|
||||||
|
|
||||||
Public Sub New()
|
|
||||||
Me._timer = New System.Timers.Timer(My.Settings.ExecutionFrequency_Minutes * 60 * 1000) With {.AutoReset = True}
|
|
||||||
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
|
|
||||||
Public Function Start(hostControl As HostControl) As Boolean Implements ServiceControl.Start
|
|
||||||
Me._timer.Start()
|
|
||||||
Return True
|
|
||||||
End Function
|
|
||||||
Public Function StartImmediately(hostControl As HostControl) As Boolean
|
|
||||||
Me._timer.Start()
|
|
||||||
System.Threading.Tasks.Task.Run(Sub()
|
|
||||||
If My.Settings.DebugDetails = True Then Call DebugToFile("fds__data_service - timer started with interval " & _timer.Interval.ToString, filename:="DebugDetail.txt")
|
|
||||||
Call update_mfr() 'start right away and do not wait until first intervall period is over
|
|
||||||
End Sub)
|
|
||||||
'do not wait and immediately return
|
|
||||||
Return True
|
|
||||||
End Function
|
|
||||||
|
|
||||||
Public Function [Stop](hostControl As HostControl) As Boolean Implements ServiceControl.Stop
|
|
||||||
Me._timer.Stop()
|
|
||||||
If My.Settings.DebugDetails = True Then System.Threading.Tasks.Task.Run(Sub() Call DebugToFile("fds__data_service - timer stopped", filename:="DebugDetail.txt"))
|
|
||||||
|
|
||||||
Return True
|
|
||||||
End Function
|
|
||||||
|
|
||||||
|
|
||||||
Public Sub timerElapsed() Handles _timer.Elapsed
|
|
||||||
Call update_mfr()
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
Friend Shared Sub update_mfr()
|
|
||||||
If My.Settings.DebugDetails = True Then Call DebugToFile("fds__data_service update_mfr UpdateIfNecessary - timer elapsed", filename:="DebugDetail.txt")
|
|
||||||
'call update to data if necessary
|
|
||||||
Try
|
|
||||||
Dim t As Threading.Tasks.Task = Threading.Tasks.Task.Run(Async Function()
|
|
||||||
'Await UpdateIfNecessary_Single_async(et:=MFR_RESTClient.generic._generic.EntityTypes.Report, DebugDetails:=My.Settings.DebugDetails)
|
|
||||||
Await UpdateIfNecessary_async(DebugDetails:=My.Settings.DebugDetails)
|
|
||||||
Await UpdateRequested_async(DebugDetails:=My.Settings.DebugDetails)
|
|
||||||
Await GetInvoiceFiles_async(DebugDetails:=My.Settings.DebugDetails)
|
|
||||||
'Await getDatevZip()
|
|
||||||
End Function)
|
|
||||||
t.Wait()
|
|
||||||
Catch ex As Exception
|
|
||||||
Call DebugLog("fds__data_service update_mfr UpdateIfNecessary", SQLConnection:=Nothing, exc:=ex)
|
|
||||||
If My.Settings.DebugDetails = True Then Call DebugToFile("fds__data_service update_mfr UpdateIfNecessary", exc:=ex, data:="", filename:="DebugDetail.txt")
|
|
||||||
End Try
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
End Class
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Public Module fds_main
|
|
||||||
|
|
||||||
Sub Main()
|
|
||||||
Dim clArgs() As String = Environment.GetCommandLineArgs()
|
|
||||||
|
|
||||||
If (New String() {"digital-pc", "digital-dpc"}).Contains(Environment.MachineName.ToLower) = False Then
|
|
||||||
|
|
||||||
HostFactory.Run(Sub(x)
|
|
||||||
x.Service(Of fds_service)(AddressOf ServiceConfiguratorCallback)
|
|
||||||
x.EnablePauseAndContinue()
|
|
||||||
x.StartAutomatically()
|
|
||||||
x.RunAsLocalSystem()
|
|
||||||
x.SetDescription("MFR Data Sync")
|
|
||||||
x.SetDisplayName("MFR Data Sync")
|
|
||||||
x.SetServiceName("MFR Data Sync")
|
|
||||||
End Sub)
|
|
||||||
Else
|
|
||||||
Call fds_service.update_mfr()
|
|
||||||
'Call DEv()
|
|
||||||
End If
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
Private Sub ServiceConfiguratorCallback(s As ServiceConfigurators.ServiceConfigurator(Of fds_service))
|
|
||||||
s.ConstructUsing(Function(name) New fds_service())
|
|
||||||
s.WhenStarted(Function(tc, Host)
|
|
||||||
Return tc.Start(Host)
|
|
||||||
End Function)
|
|
||||||
s.WhenStopped(Function(tc, Host)
|
|
||||||
Return tc.Stop(Host)
|
|
||||||
End Function)
|
|
||||||
s.BeforeStoppingService(Sub(HostStopContext)
|
|
||||||
If My.Settings.DebugDetails = True Then System.Threading.Tasks.Task.Run(Sub() Call DebugToFile("fds__data_service - beforestop", filename:="DebugDetail.txt"))
|
|
||||||
End Sub)
|
|
||||||
s.WhenPaused(Function(tc, Host)
|
|
||||||
Return tc.Stop(Host)
|
|
||||||
End Function)
|
|
||||||
s.WhenContinued(Function(tc, Host)
|
|
||||||
Return tc.StartImmediately(Host)
|
|
||||||
End Function)
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Public Sub DEv()
|
|
||||||
Using MFR As New fds_MFR_Client()
|
|
||||||
'Diagnostics.Debug.Print(MFR.ReadAnything(address:="https://portal.mobilefieldreport.com/odata/$metadata"))
|
|
||||||
'Diagnostics.Debug.Print(MFR.ReadAnything(address:="https://portal.mobilefieldreport.com/odata/Companies?$top=5&$expand=Contacts,Tags,ServiceObjects,MainContact"))
|
|
||||||
'Diagnostics.Debug.Print(MFR.ReadAnything(address:="https://portal.mobilefieldreport.com/odata/ServiceObjects?$expand=WarehouseManager,CustomValueSteps,Company,Product,Tags,ChildServiceObject,Contacts,Items"))
|
|
||||||
'Diagnostics.Debug.Print(MFR.ReadAnything(address:="https://portal.mobilefieldreport.com/odata/Contacts/$count"))
|
|
||||||
'Diagnostics.Debug.Print(MFR.getEntities())
|
|
||||||
Dim fle As Byte()
|
|
||||||
Try
|
|
||||||
fle = MFR.GetFile("https://portal.mobilefieldreport.com/mfr/Report/19584712737/Content/")
|
|
||||||
System.IO.File.WriteAllBytes("C:\Users\sailo\Desktop\Test.pdf", fle)
|
|
||||||
Catch ex As Exception
|
|
||||||
|
|
||||||
End Try
|
|
||||||
End Using
|
|
||||||
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
End Module
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Partial Friend Module fds_debug
|
|
||||||
|
|
||||||
|
|
||||||
Public Sub DebugLog(CodeReference As String, Optional exc As Exception = Nothing, Optional data As String = "", Optional context As Object = Nothing, Optional execute_async As Boolean = True)
|
|
||||||
If execute_async = True Then
|
|
||||||
Call DebugLog_async(CodeReference:=CodeReference, SQLConnectionString:=FDSConnectionString(), exc:=exc, data:=data, context:=context)
|
|
||||||
Else
|
|
||||||
Call DebugLog_sync(CodeReference:=CodeReference, SQLConnectionString:=FDSConnectionString(), exc:=exc, data:=data, context:=context)
|
|
||||||
End If
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
End Module
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,226 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
'''' <summary>
|
|
||||||
'''' Returns a delimited <see cref="String" /> containing the field values from a <see cref="DataRow" />.
|
|
||||||
'''' </summary>
|
|
||||||
'''' <param name="source">
|
|
||||||
'''' The input <see cref="DataRow" />.
|
|
||||||
'''' </param>
|
|
||||||
'''' <param name="delimiter">
|
|
||||||
'''' The delimiter placed between field values. the default value is a comma.
|
|
||||||
'''' </param>
|
|
||||||
'''' <returns>
|
|
||||||
'''' A <see cref="String"/> containing the field values from the row separated by the specified delimiter.
|
|
||||||
'''' </returns>
|
|
||||||
'<Runtime.CompilerServices.Extension>
|
|
||||||
'Public Function ToCsv(source As DataRow,
|
|
||||||
' Optional delimiter As String = ",") As String
|
|
||||||
' Return String.Join(delimiter, source.ItemArray)
|
|
||||||
'End Function
|
|
||||||
|
|
||||||
''' <summary>
|
|
||||||
''' Returns a delimited <see cref="String" /> containing the field values from a <see cref="DataRow" />.
|
|
||||||
''' </summary>
|
|
||||||
''' <param name="source">
|
|
||||||
''' The input <see cref="DataRow" />.
|
|
||||||
''' </param>
|
|
||||||
''' <param name="quoteStrings">
|
|
||||||
''' <b>True</b> to wrap <see cref="String"/> values in double-quotes; otherwise, <b>False</b>.
|
|
||||||
''' If double-quotes are added, double-quotes within text are escaped with another double-quote.
|
|
||||||
''' </param>
|
|
||||||
''' <param name="delimiter">
|
|
||||||
''' The delimiter placed between field values. the default value is a comma.
|
|
||||||
''' </param>
|
|
||||||
''' <returns>
|
|
||||||
''' A <see cref="String"/> containing the field values from the row separated by the specified delimiter.
|
|
||||||
''' </returns>
|
|
||||||
<Runtime.CompilerServices.Extension>
|
|
||||||
Public Function ToCsv(source As DataRow,
|
|
||||||
quoteStrings As Boolean,
|
|
||||||
cultureinfo As Globalization.CultureInfo,
|
|
||||||
Optional delimiter As String = ",") As String
|
|
||||||
Dim fieldValues = source.ItemArray
|
|
||||||
|
|
||||||
|
|
||||||
Dim rx As New Text.RegularExpressions.Regex("(\"")")
|
|
||||||
'Wrap any String values in double-quotes and also escape any double-quotes in the String with another double-quote.
|
|
||||||
'replace array by converted array
|
|
||||||
fieldValues = fieldValues.Select(Function(o)
|
|
||||||
If IsNothing(o) OrElse IsDBNull(o) Then
|
|
||||||
Return ""
|
|
||||||
ElseIf o.GetType = GetType(String) Then
|
|
||||||
If quoteStrings = True Then
|
|
||||||
Return Microsoft.VisualBasic.ChrW(34) & rx.Replace(o.ToString, Microsoft.VisualBasic.ChrW(34) & Microsoft.VisualBasic.ChrW(34)) & Microsoft.VisualBasic.ChrW(34)
|
|
||||||
Else
|
|
||||||
Return o.ToString
|
|
||||||
End If
|
|
||||||
Else
|
|
||||||
Select Case o.GetType
|
|
||||||
Case GetType(Decimal)
|
|
||||||
Return DirectCast(o, Decimal).ToString(cultureinfo)
|
|
||||||
Case GetType(Single)
|
|
||||||
Return DirectCast(o, Single).ToString(cultureinfo)
|
|
||||||
Case GetType(Double)
|
|
||||||
Return DirectCast(o, Double).ToString(cultureinfo)
|
|
||||||
Case GetType(Boolean)
|
|
||||||
Return DirectCast(o, Boolean).ToString(cultureinfo)
|
|
||||||
Case GetType(System.DateTime)
|
|
||||||
Return DirectCast(o, DateTime).ToUniversalTime.ToString("U")
|
|
||||||
Case Else
|
|
||||||
Return o.ToString()
|
|
||||||
End Select
|
|
||||||
End If
|
|
||||||
End Function).ToArray()
|
|
||||||
|
|
||||||
|
|
||||||
Return String.Join(delimiter, fieldValues)
|
|
||||||
End Function
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
''' <summary>
|
|
||||||
''' Returns a delimited <see cref="String" /> containing the field values from the rows a <see cref="DataTable" />.
|
|
||||||
''' </summary>
|
|
||||||
''' <param name="source">The input <see cref="DataTable" />.</param>
|
|
||||||
''' <param name="includeHeaders"><b>True</b> to include a row of column headers; otherwise, <b>False</b></param>
|
|
||||||
''' <param name="quoteStrings"><b>True</b> to wrap <see cref="String"/> values in double-quotes; otherwise, <b>False</b>.
|
|
||||||
''' If double-quotes are added, double-quotes within text are escaped with another double-quote.</param>
|
|
||||||
''' <param name="rowDelimiter">The delimiter placed between rows. the default value is a line break comprising a carriage return and a line feed.</param>
|
|
||||||
''' <param name="fieldDelimiter">The delimiter placed between field values. the default value is a comma.</param>
|
|
||||||
''' <param name="cultureinfo">The culture that is used to convert float-point numbers like <see cref="Decimal" /> or <see cref="Double"/> to string. <br/>This falls back to InvariantCulture, if not provided.</param>
|
|
||||||
''' <param name="quoteHeader"><b>True</b> to wrap <see cref="String"/> column header names in double-quotes; otherwise, <b>False</b>.<br />
|
|
||||||
''' If no value is provided, the settings falls back to <b>quoteStrings</b> parameter.</param>
|
|
||||||
''' <returns>A <see cref="String"/> containing the field values from the rows of the table separated by the specified delimiters.</returns>
|
|
||||||
<Runtime.CompilerServices.Extension>
|
|
||||||
Public Function ToCsv(source As DataTable,
|
|
||||||
includeHeaders As Boolean,
|
|
||||||
quoteStrings As Boolean,
|
|
||||||
Optional rowDelimiter As String = ControlChars.CrLf,
|
|
||||||
Optional fieldDelimiter As String = ",",
|
|
||||||
Optional cultureinfo As Globalization.CultureInfo = Nothing,
|
|
||||||
Optional quoteHeader As Boolean? = Nothing) As String
|
|
||||||
If quoteHeader.HasValue = False Then quoteHeader = quoteStrings
|
|
||||||
cultureinfo = If(cultureinfo, Globalization.CultureInfo.InvariantCulture) 'fallback if not provided
|
|
||||||
Dim rows = source.Rows.
|
|
||||||
Cast(Of DataRow)().
|
|
||||||
Select(Function(row) row.ToCsv(quoteStrings:=quoteStrings, cultureinfo:=cultureinfo, delimiter:=fieldDelimiter))
|
|
||||||
|
|
||||||
If includeHeaders = True Then
|
|
||||||
Dim rx As New Text.RegularExpressions.Regex("(\"")")
|
|
||||||
Dim headers = String.Join(fieldDelimiter,
|
|
||||||
source.Columns.
|
|
||||||
Cast(Of DataColumn)().
|
|
||||||
Select(Function(column) If(quoteHeader.Value,
|
|
||||||
Microsoft.VisualBasic.ChrW(34) & rx.Replace(column.ColumnName.ToString, Microsoft.VisualBasic.ChrW(34) & Microsoft.VisualBasic.ChrW(34)) & Microsoft.VisualBasic.ChrW(34),
|
|
||||||
column.ColumnName)))
|
|
||||||
|
|
||||||
rows = {headers}.Concat(rows)
|
|
||||||
End If
|
|
||||||
|
|
||||||
Return String.Join(rowDelimiter, rows)
|
|
||||||
End Function
|
|
||||||
|
|
||||||
''' <summary>
|
|
||||||
''' Returns a text-file containing the string, created by streamwriter.
|
|
||||||
''' </summary>
|
|
||||||
''' <param name="input">The input <see cref="String"/>.</param>
|
|
||||||
''' <param name="encoding">The encoding used with streamwriter for the textfile. This falls back to <see cref="System.Text.Encoding.utf8"/>, if not provided.</param>
|
|
||||||
''' <returns>A file as byte-array.</returns>
|
|
||||||
<Runtime.CompilerServices.Extension>
|
|
||||||
Public Function ToByteArray(input As String, Optional encoding As System.Text.Encoding = Nothing) As Byte()
|
|
||||||
Dim content As Byte() = Nothing
|
|
||||||
Using ms As New IO.MemoryStream
|
|
||||||
Using sw As New IO.StreamWriter(ms, encoding:=If(encoding, System.Text.Encoding.UTF8))
|
|
||||||
sw.Write(input)
|
|
||||||
sw.Flush()
|
|
||||||
ms.Position = 0
|
|
||||||
content = ms.ToArray()
|
|
||||||
End Using
|
|
||||||
End Using
|
|
||||||
Return content
|
|
||||||
End Function
|
|
||||||
|
|
||||||
|
|
||||||
Public Function WriteStreamToDisk(ByVal StreamToWrite As IO.Stream, ByVal FilePath As String) As Boolean
|
|
||||||
'Dim tmpFilePath As String = Left(FilePath, Len(FilePath) - 4) & ".tmp"
|
|
||||||
Dim cnt = 0
|
|
||||||
restart:
|
|
||||||
Try
|
|
||||||
If My.Computer.FileSystem.FileExists(FilePath) Then My.Computer.FileSystem.DeleteFile(FilePath)
|
|
||||||
Using FleStream As System.IO.FileStream = New System.IO.FileStream(FilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.Delete)
|
|
||||||
ReadWriteStream(StreamToWrite, FleStream, True)
|
|
||||||
End Using
|
|
||||||
Catch ex As Exception
|
|
||||||
System.Diagnostics.Debug.WriteLine($"{"WriteStreamToDisk - " & ex.Message}")
|
|
||||||
cnt += 1
|
|
||||||
If cnt = 6 Then
|
|
||||||
Return False
|
|
||||||
Exit Function
|
|
||||||
Else
|
|
||||||
Threading.Thread.Sleep(500)
|
|
||||||
GoTo restart
|
|
||||||
End If
|
|
||||||
End Try
|
|
||||||
Return True
|
|
||||||
End Function
|
|
||||||
|
|
||||||
Public Function ReadWriteStream(ByVal readStream As IO.Stream, ByVal writeStream As IO.Stream, ByVal closeWriteStream As Boolean) As Boolean
|
|
||||||
Try
|
|
||||||
Dim Length As Integer = 256
|
|
||||||
Dim buffer(Length - 1) As Byte
|
|
||||||
readStream.Seek(0, System.IO.SeekOrigin.Begin)
|
|
||||||
Dim bytesRead As Integer = readStream.Read(buffer, 0, Length)
|
|
||||||
'write the required bytes
|
|
||||||
While (bytesRead > 0)
|
|
||||||
writeStream.Write(buffer, 0, bytesRead)
|
|
||||||
bytesRead = readStream.Read(buffer, 0, Length)
|
|
||||||
End While
|
|
||||||
readStream.Close()
|
|
||||||
If closeWriteStream = True Then writeStream.Close()
|
|
||||||
Return True
|
|
||||||
Catch ex As Exception
|
|
||||||
System.Diagnostics.Debug.WriteLine($"{"ReadWriteStream - " & ex.Message}")
|
|
||||||
Call OCMS.debug_log("files_folders ReadWriteStream", ex)
|
|
||||||
Return False
|
|
||||||
End Try
|
|
||||||
End Function
|
|
||||||
|
|
||||||
<System.Diagnostics.DebuggerStepThrough()>
|
|
||||||
<Runtime.CompilerServices.Extension()>
|
|
||||||
Public Function NameBase(ByVal FI As System.IO.FileInfo) As String
|
|
||||||
Return FI.Name.Substring(startIndex:=0, length:=FI.Name.Length - FI.Extension.Length)
|
|
||||||
End Function
|
|
||||||
|
|
||||||
|
|
||||||
<System.Diagnostics.DebuggerStepThrough()>
|
|
||||||
<Runtime.CompilerServices.Extension()>
|
|
||||||
Public Function MimeType(ByVal FI As System.IO.FileInfo) As String
|
|
||||||
Return System.Web.MimeMapping.GetMimeMapping(FI.Name)
|
|
||||||
End Function
|
|
||||||
End Module
|
|
||||||
@@ -1,500 +0,0 @@
|
|||||||
Imports SevenZip 'Squid-Box.SevenZipSharp
|
|
||||||
Imports System.IO
|
|
||||||
|
|
||||||
Namespace Global.fds
|
|
||||||
Public Class Archive
|
|
||||||
Implements IDisposable
|
|
||||||
|
|
||||||
'Private Declare Function WaitForSingleObject Lib "kernel32" (ByVal hHandle As Long, ByVal dwMilliseconds As Long) As Long
|
|
||||||
'Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
|
|
||||||
|
|
||||||
'Public Enum Timeunit As Long
|
|
||||||
' Milliseconds = 0
|
|
||||||
' Seconds = 1000
|
|
||||||
' Minutes = 60000
|
|
||||||
'End Enum
|
|
||||||
'Public Sub Wait(ByVal No As Integer, ByVal unit As Timeunit)
|
|
||||||
' Dim tme As Long = CLng(No * unit)
|
|
||||||
' Sleep(tme)
|
|
||||||
'End Sub
|
|
||||||
|
|
||||||
Public Event Saving()
|
|
||||||
Public Event FileSaved()
|
|
||||||
Public Event FileStreamCreated()
|
|
||||||
|
|
||||||
Private _ArchiveFile As FileInfo
|
|
||||||
Private _ArchivePassword As String
|
|
||||||
Private _ArchiveFormat As OutArchiveFormat
|
|
||||||
Public TempPath As String = System.AppDomain.CurrentDomain.BaseDirectory
|
|
||||||
Public Property ArchiveFileStream As IO.Stream
|
|
||||||
|
|
||||||
Private ZipOut As SevenZipExtractor
|
|
||||||
Private ZipIn As SevenZipCompressor
|
|
||||||
Public ZipAppend As Boolean = True
|
|
||||||
|
|
||||||
Public ExitOK As Boolean = False
|
|
||||||
Public ZipInOK As Boolean = False
|
|
||||||
|
|
||||||
Public Sub New(ByVal ArchiveFile As FileInfo, Optional ByVal ArchivePassword As String = "", Optional ByVal INIT As Boolean = True, Optional ByVal Type As OutArchiveFormat = OutArchiveFormat.SevenZip)
|
|
||||||
Me._ArchiveFormat = Type
|
|
||||||
Me._ArchiveFile = New FileInfo(ArchiveFile.FullName.Replace(ArchiveFile.Extension, If(Type = OutArchiveFormat.SevenZip, ".7z", ArchiveFile.Extension)))
|
|
||||||
Me._ArchivePassword = ArchivePassword
|
|
||||||
If INIT = True Then Call InitZipIn(Type)
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
Private Sub InitZipIn(ByVal Type As OutArchiveFormat)
|
|
||||||
Dim assemblydirectory As IO.DirectoryInfo
|
|
||||||
If Zipping.SevenZipPath = "" Then
|
|
||||||
Try
|
|
||||||
assemblydirectory = New IO.DirectoryInfo(New Uri(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase)).LocalPath)
|
|
||||||
Dim zip As IO.FileInfo = assemblydirectory.GetFiles("7z.dll", SearchOption.AllDirectories).FirstOrDefault
|
|
||||||
Zipping.SevenZipPath = If(IsNothing(zip), "", zip.FullName)
|
|
||||||
Finally
|
|
||||||
If Zipping.SevenZipPath = "" Then
|
|
||||||
assemblydirectory = New IO.DirectoryInfo(System.AppDomain.CurrentDomain.BaseDirectory)
|
|
||||||
Dim zip As IO.FileInfo = assemblydirectory.GetFiles("7z.dll", SearchOption.AllDirectories).FirstOrDefault
|
|
||||||
Zipping.SevenZipPath = If(IsNothing(zip), "", zip.FullName)
|
|
||||||
End If
|
|
||||||
End Try
|
|
||||||
If Zipping.SevenZipPath = "" Then
|
|
||||||
OCMS.debug_log("DDA.intranet.Zipping Archive InitZipIn", error:="SevenZipPath not found")
|
|
||||||
End If
|
|
||||||
End If
|
|
||||||
SevenZipCompressor.SetLibraryPath(SevenZipPath)
|
|
||||||
|
|
||||||
Me.ZipIn = New SevenZipCompressor
|
|
||||||
With Me.ZipIn
|
|
||||||
If Type = OutArchiveFormat.SevenZip AndAlso Me._ArchiveFile.Extension.ToLower.Contains("7z") = True Then
|
|
||||||
.ArchiveFormat = OutArchiveFormat.SevenZip
|
|
||||||
Else
|
|
||||||
.ArchiveFormat = Type
|
|
||||||
End If
|
|
||||||
|
|
||||||
.CompressionLevel = SevenZip.CompressionLevel.Ultra
|
|
||||||
Select Case .ArchiveFormat
|
|
||||||
Case OutArchiveFormat.SevenZip
|
|
||||||
.CompressionMethod = SevenZip.CompressionMethod.Lzma2
|
|
||||||
Case OutArchiveFormat.Zip, OutArchiveFormat.GZip
|
|
||||||
.CompressionMethod = CompressionMethod.Deflate
|
|
||||||
Case Else
|
|
||||||
.CompressionMethod = CompressionMethod.Default
|
|
||||||
End Select
|
|
||||||
|
|
||||||
If ZipAppend = True Then
|
|
||||||
.CompressionMode = SevenZip.CompressionMode.Append
|
|
||||||
Else
|
|
||||||
.CompressionMode = SevenZip.CompressionMode.Create
|
|
||||||
End If
|
|
||||||
.DirectoryStructure = False
|
|
||||||
End With
|
|
||||||
Me.ZipInOK = True
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
Public Sub Extract(ByVal DataArchiveFilePath As FileInfo, ByVal TgtDirectory As DirectoryInfo, Optional ByVal Type As OutArchiveFormat = Nothing)
|
|
||||||
If DataArchiveFilePath.Exists Then
|
|
||||||
If IsNothing(Type) = True AndAlso DataArchiveFilePath.Extension.ToLower.Contains("7z") = True Then
|
|
||||||
Type = OutArchiveFormat.SevenZip
|
|
||||||
ElseIf IsNothing(Type) = True Then
|
|
||||||
Type = OutArchiveFormat.Zip
|
|
||||||
End If
|
|
||||||
|
|
||||||
If Me.ZipInOK = False Then Call InitZipIn(Type)
|
|
||||||
|
|
||||||
If Me._ArchivePassword = "" Then
|
|
||||||
Me.ZipOut = New SevenZipExtractor(DataArchiveFilePath.FullName)
|
|
||||||
Else
|
|
||||||
Me.ZipOut = New SevenZipExtractor(DataArchiveFilePath.FullName, Me._ArchivePassword)
|
|
||||||
End If
|
|
||||||
|
|
||||||
Try
|
|
||||||
If Me.ZipOut.ArchiveFileData(0).Encrypted = False And Not Me._ArchivePassword = "" Then
|
|
||||||
Me._ArchivePassword = ""
|
|
||||||
End If
|
|
||||||
Catch ex As Exception
|
|
||||||
System.Diagnostics.Debug.WriteLine($"{"zip Extract - " & ex.Message}")
|
|
||||||
OCMS.debug_log("DDA.intranet.Zipping Archive InitZipIn", ex:=ex, data:=New With {.DataArchiveFilePath = DataArchiveFilePath.FullName, .TgtDirectory = TgtDirectory.FullName})
|
|
||||||
Exit Sub
|
|
||||||
End Try
|
|
||||||
|
|
||||||
If Not ZipOut Is Nothing Then
|
|
||||||
Me.ZipOut.ExtractArchive(TgtDirectory.FullName)
|
|
||||||
Me.ZipOut.Dispose()
|
|
||||||
End If
|
|
||||||
End If
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
Public Function FileInfo_to_Filepaths_Converter() As Converter(Of FileInfo, String)
|
|
||||||
Return New Converter(Of FileInfo, String)(Function(filepath As FileInfo) filepath.FullName())
|
|
||||||
End Function
|
|
||||||
Public Function Filepaths_to_FileInfo_Converter() As Converter(Of String, FileInfo)
|
|
||||||
Return New Converter(Of String, FileInfo)(Function(filepath As String) New FileInfo(filepath))
|
|
||||||
End Function
|
|
||||||
|
|
||||||
Public Function Compress(ByVal FilePaths As List(Of String), Optional ByVal ArchiveFilePath As String = Nothing, Optional ByVal ArchivePass As String = Nothing, Optional ByVal Type As OutArchiveFormat = OutArchiveFormat.SevenZip) As Boolean
|
|
||||||
Return Compress(Files:=FilePaths.ConvertAll(Filepaths_to_FileInfo_Converter()), ArchiveFile:=If(IsNothing(ArchiveFilePath), Nothing, New FileInfo(ArchiveFilePath)), ArchivePass:=ArchivePass, Type:=Type)
|
|
||||||
End Function
|
|
||||||
Public Function Compress(ByVal Files As List(Of FileInfo), Optional ByVal ArchiveFile As FileInfo = Nothing, Optional ByVal ArchivePass As String = Nothing, Optional ByVal Type As OutArchiveFormat = OutArchiveFormat.SevenZip) As Boolean
|
|
||||||
If Files.Count = 0 Then Return True
|
|
||||||
If Me.ZipInOK = False Then Call InitZipIn(Type)
|
|
||||||
|
|
||||||
If IsNothing(ArchiveFile) = True Then ArchiveFile = Me._ArchiveFile
|
|
||||||
If If(IsNothing(ArchivePass), "", ArchivePass) = "" Then ArchivePass = Me._ArchivePassword
|
|
||||||
|
|
||||||
If ArchiveFile.Exists() AndAlso Me.ZipAppend = True Then
|
|
||||||
Me.ZipIn.CompressionMode = CompressionMode.Append
|
|
||||||
Else
|
|
||||||
If ArchiveFile.Exists = True Then ArchiveFile.Delete()
|
|
||||||
Me.ZipIn.CompressionMode = CompressionMode.Create
|
|
||||||
End If
|
|
||||||
|
|
||||||
Try
|
|
||||||
Dim FilesVerified As FileInfo() = Files.Where(Function(f As FileInfo) f.Exists).ToArray()
|
|
||||||
Dim FilePaths As String() = Array.ConvertAll(Of FileInfo, String)(FilesVerified, FileInfo_to_Filepaths_Converter())
|
|
||||||
If ArchivePass = "" Then
|
|
||||||
Me.ZipIn.CompressFiles(ArchiveFile.FullName, FilePaths)
|
|
||||||
Else
|
|
||||||
Me.ZipIn.EncryptHeaders = True
|
|
||||||
Me.ZipIn.ZipEncryptionMethod = ZipEncryptionMethod.Aes256
|
|
||||||
Me.ZipIn.CompressFilesEncrypted(ArchiveFile.FullName, ArchivePass, FilePaths)
|
|
||||||
End If
|
|
||||||
RaiseEvent FileSaved()
|
|
||||||
|
|
||||||
'Debug.Print("Saved: " & Now().ToString)
|
|
||||||
Me.ExitOK = True
|
|
||||||
'Disposing...
|
|
||||||
Me.ZipIn = Nothing
|
|
||||||
Me.ZipInOK = False
|
|
||||||
Catch ex As Exception
|
|
||||||
'Debug.Print("NOT Saved: " & Now().ToString)
|
|
||||||
Me.ExitOK = False
|
|
||||||
End Try
|
|
||||||
Return Me.ExitOK AndAlso ArchiveFile.Exists
|
|
||||||
End Function
|
|
||||||
Public Function Compress(ByVal FilePath As String) As Boolean
|
|
||||||
Dim FL As New List(Of String) From {
|
|
||||||
FilePath
|
|
||||||
}
|
|
||||||
Call Compress(FL)
|
|
||||||
Return True
|
|
||||||
End Function
|
|
||||||
Public Function CompressToStream(ByVal FilePath As String) As Boolean
|
|
||||||
Dim FL As New List(Of String) From {
|
|
||||||
FilePath
|
|
||||||
}
|
|
||||||
Call CompressToStream(FL)
|
|
||||||
Return True
|
|
||||||
End Function
|
|
||||||
|
|
||||||
Public Function CompressToStream(ByVal FilePaths As List(Of String)) As Boolean
|
|
||||||
Return CompressToStream(Files:=FilePaths.ConvertAll(Filepaths_to_FileInfo_Converter()))
|
|
||||||
End Function
|
|
||||||
Public Function CompressToStream(ByVal Files As List(Of FileInfo)) As Boolean
|
|
||||||
If Files.Count = 0 Then Return True
|
|
||||||
If Me.ZipInOK = False Then Call InitZipIn(Me._ArchiveFormat)
|
|
||||||
|
|
||||||
If IsNothing(_ArchiveFileStream) Then 'nur wenn der interne leer ist...
|
|
||||||
Me.ZipIn.CompressionMode = CompressionMode.Create
|
|
||||||
Me._ArchiveFileStream = New MemoryStream
|
|
||||||
Else
|
|
||||||
Me.ZipIn.CompressionMode = CompressionMode.Append
|
|
||||||
End If
|
|
||||||
|
|
||||||
Try
|
|
||||||
Dim FilesVerified As FileInfo() = Files.Where(Function(f As FileInfo) f.Exists).ToArray()
|
|
||||||
Dim FilePaths As String() = Array.ConvertAll(Of FileInfo, String)(FilesVerified, FileInfo_to_Filepaths_Converter())
|
|
||||||
If Me._ArchivePassword = "" Then
|
|
||||||
Me.ZipIn.CompressFiles(Me._ArchiveFileStream, FilePaths)
|
|
||||||
Else
|
|
||||||
Me.ZipIn.EncryptHeaders = True
|
|
||||||
Me.ZipIn.ZipEncryptionMethod = ZipEncryptionMethod.Aes256
|
|
||||||
Me.ZipIn.CompressFilesEncrypted(Me._ArchiveFileStream, Me._ArchivePassword, FilePaths)
|
|
||||||
End If
|
|
||||||
Me._ArchiveFileStream.Seek(0, SeekOrigin.Begin)
|
|
||||||
RaiseEvent FileStreamCreated()
|
|
||||||
|
|
||||||
'Debug.Print("Saved: " & Now().ToString)
|
|
||||||
Me.ExitOK = True
|
|
||||||
'Disposing...
|
|
||||||
Me.ZipIn = Nothing
|
|
||||||
Me.ZipInOK = False
|
|
||||||
Catch ex As Exception
|
|
||||||
'Debug.Print("NOT Saved: " & Now().ToString)
|
|
||||||
Me.ExitOK = False
|
|
||||||
End Try
|
|
||||||
Return Me.ExitOK
|
|
||||||
End Function
|
|
||||||
|
|
||||||
Public Function CompressToStream(ByVal Files As List(Of FileInfo), ByRef TargetStream As IO.Stream) As Boolean
|
|
||||||
If Files.Count = 0 Then Return True
|
|
||||||
If Me.ZipInOK = False Then Call InitZipIn(Me._ArchiveFormat)
|
|
||||||
|
|
||||||
If IsNothing(TargetStream) = True Then
|
|
||||||
TargetStream = New MemoryStream
|
|
||||||
End If
|
|
||||||
Me.ZipIn.CompressionMode = CompressionMode.Create
|
|
||||||
Dim FilePaths As String() = New String() {}
|
|
||||||
Try
|
|
||||||
Dim FilesVerified As FileInfo() = Files.Where(Function(f As FileInfo) f.Exists).ToArray()
|
|
||||||
FilePaths = Array.ConvertAll(Of FileInfo, String)(FilesVerified, FileInfo_to_Filepaths_Converter())
|
|
||||||
If Me._ArchivePassword = "" Then
|
|
||||||
Me.ZipIn.CompressFiles(TargetStream, FilePaths)
|
|
||||||
Else
|
|
||||||
Me.ZipIn.EncryptHeaders = True
|
|
||||||
Me.ZipIn.ZipEncryptionMethod = ZipEncryptionMethod.Aes256
|
|
||||||
Me.ZipIn.CompressFilesEncrypted(TargetStream, Me._ArchivePassword, FilePaths)
|
|
||||||
End If
|
|
||||||
TargetStream.Seek(0, SeekOrigin.Begin)
|
|
||||||
|
|
||||||
'Debug.Print("Saved: " & Now().ToString)
|
|
||||||
Me.ExitOK = True
|
|
||||||
'Disposing...
|
|
||||||
Me.ZipIn = Nothing
|
|
||||||
Me.ZipInOK = False
|
|
||||||
Catch ex As Exception
|
|
||||||
'Debug.Print("NOT Saved: " & Now().ToString)
|
|
||||||
OCMS.debug_log("IntranetController zip", ex, data:=New With {.filepaths = FilePaths})
|
|
||||||
Me.ExitOK = False
|
|
||||||
End Try
|
|
||||||
Return Me.ExitOK
|
|
||||||
End Function
|
|
||||||
|
|
||||||
|
|
||||||
Public Function CompressToStream(ByVal Files As Dictionary(Of String, IO.Stream)) As Boolean
|
|
||||||
If Files.Count = 0 Then Return True
|
|
||||||
If Me.ZipInOK = False Then Call InitZipIn(Me._ArchiveFormat)
|
|
||||||
|
|
||||||
If IsNothing(_ArchiveFileStream) Then 'nur wenn der interne leer ist...
|
|
||||||
Me.ZipIn.CompressionMode = CompressionMode.Create
|
|
||||||
Me._ArchiveFileStream = New MemoryStream
|
|
||||||
Else
|
|
||||||
Me.ZipIn.CompressionMode = CompressionMode.Append
|
|
||||||
End If
|
|
||||||
|
|
||||||
Try
|
|
||||||
If Me._ArchivePassword = "" Then
|
|
||||||
Me.ZipIn.CompressStreamDictionary(streamDictionary:=Files, Me._ArchiveFileStream)
|
|
||||||
Else
|
|
||||||
Me.ZipIn.EncryptHeaders = True
|
|
||||||
Me.ZipIn.ZipEncryptionMethod = ZipEncryptionMethod.Aes256
|
|
||||||
Me.ZipIn.CompressStreamDictionary(streamDictionary:=Files, Me._ArchiveFileStream, password:=Me._ArchivePassword)
|
|
||||||
End If
|
|
||||||
Me._ArchiveFileStream.Seek(0, SeekOrigin.Begin)
|
|
||||||
RaiseEvent FileStreamCreated()
|
|
||||||
|
|
||||||
'Debug.Print("Saved: " & Now().ToString)
|
|
||||||
Me.ExitOK = True
|
|
||||||
'Disposing...
|
|
||||||
Me.ZipIn = Nothing
|
|
||||||
Me.ZipInOK = False
|
|
||||||
Catch ex As Exception
|
|
||||||
'Debug.Print("NOT Saved: " & Now().ToString)
|
|
||||||
Me.ExitOK = False
|
|
||||||
End Try
|
|
||||||
Return Me.ExitOK
|
|
||||||
End Function
|
|
||||||
Public Function CompressToStream(ByVal Files As Dictionary(Of String, Byte()), Optional targetstream As IO.Stream = Nothing) As Boolean
|
|
||||||
If Files.Count = 0 Then Return True
|
|
||||||
If Me.ZipInOK = False Then Call InitZipIn(Me._ArchiveFormat)
|
|
||||||
|
|
||||||
If IsNothing(_ArchiveFileStream) Then 'nur wenn der interne leer ist...
|
|
||||||
Me.ZipIn.CompressionMode = CompressionMode.Create
|
|
||||||
Me._ArchiveFileStream = New MemoryStream
|
|
||||||
Else
|
|
||||||
Me.ZipIn.CompressionMode = CompressionMode.Append
|
|
||||||
End If
|
|
||||||
|
|
||||||
Try
|
|
||||||
Dim FilesStreams As New Dictionary(Of String, IO.Stream)
|
|
||||||
For Each fy As String In Files.Keys
|
|
||||||
FilesStreams.Add(fy, New IO.MemoryStream(Files(fy)))
|
|
||||||
Next
|
|
||||||
|
|
||||||
If Me._ArchivePassword = "" Then
|
|
||||||
Me.ZipIn.CompressStreamDictionary(streamDictionary:=FilesStreams, If(IsNothing(targetstream), Me._ArchiveFileStream, targetstream))
|
|
||||||
Else
|
|
||||||
Me.ZipIn.EncryptHeaders = True
|
|
||||||
Me.ZipIn.ZipEncryptionMethod = ZipEncryptionMethod.Aes256
|
|
||||||
Me.ZipIn.CompressStreamDictionary(streamDictionary:=FilesStreams, If(IsNothing(targetstream), Me._ArchiveFileStream, targetstream), password:=Me._ArchivePassword)
|
|
||||||
End If
|
|
||||||
Me._ArchiveFileStream.Seek(0, SeekOrigin.Begin)
|
|
||||||
RaiseEvent FileStreamCreated()
|
|
||||||
|
|
||||||
'Debug.Print("Saved: " & Now().ToString)
|
|
||||||
Me.ExitOK = True
|
|
||||||
'Disposing...
|
|
||||||
Me.ZipIn = Nothing
|
|
||||||
Me.ZipInOK = False
|
|
||||||
Catch ex As Exception
|
|
||||||
'Debug.Print("NOT Saved: " & Now().ToString)
|
|
||||||
Me.ExitOK = False
|
|
||||||
End Try
|
|
||||||
Return Me.ExitOK
|
|
||||||
End Function
|
|
||||||
|
|
||||||
|
|
||||||
Public Function WriteArchiveStreamToDisk(Optional ByVal ArchiveFile As IO.FileInfo = Nothing) As Boolean
|
|
||||||
Try
|
|
||||||
If Me._ArchiveFile.Exists() Then Me._ArchiveFile.Delete()
|
|
||||||
Catch ex As Exception
|
|
||||||
End Try
|
|
||||||
If IsNothing(ArchiveFile) = False Then 'Wenn ein DateiPfad hier übergeben wurde...
|
|
||||||
WriteStreamToDisk(Me._ArchiveFileStream, ArchiveFile.FullName)
|
|
||||||
Else ' sonst wird der interne genommen
|
|
||||||
WriteStreamToDisk(Me._ArchiveFileStream, Me._ArchiveFile.FullName)
|
|
||||||
End If
|
|
||||||
Return Me._ArchiveFile.Exists
|
|
||||||
End Function
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
'Private ArchiveMail As EMail = Nothing
|
|
||||||
'Public Function SendZip(Optional ByVal Subject As String = Nothing, Optional ByVal BodyText As String = Nothing) As Boolean
|
|
||||||
' Try
|
|
||||||
' If ArchiveMail Is Nothing Then Exit Function
|
|
||||||
' If Not Subject = Nothing Then
|
|
||||||
' ArchiveMail.Subject = Subject
|
|
||||||
' End If
|
|
||||||
' If Not BodyText = Nothing Then
|
|
||||||
' ArchiveMail.Body = BodyText
|
|
||||||
' End If
|
|
||||||
' 'Send with archive as attachment
|
|
||||||
' Return ArchiveMail.Send(_ArchivePath.Path)
|
|
||||||
' Catch ex As Exception
|
|
||||||
' Return False
|
|
||||||
' End Try
|
|
||||||
'End Function
|
|
||||||
'Public Function SendZipStream(Optional ByVal Subject As String = Nothing, Optional ByVal BodyText As String = Nothing) As Boolean
|
|
||||||
' Try
|
|
||||||
' If ArchiveMail Is Nothing Then Exit Function
|
|
||||||
' If Not Subject = Nothing Then
|
|
||||||
' ArchiveMail.Subject = Subject
|
|
||||||
' End If
|
|
||||||
' If Not BodyText = Nothing Then
|
|
||||||
' ArchiveMail.Body = BodyText
|
|
||||||
' End If
|
|
||||||
' 'Send with archive as attachment
|
|
||||||
' Return ArchiveMail.Send(_ArchivePath.Name, _ArchiveFileStream)
|
|
||||||
' Catch ex As Exception
|
|
||||||
' Return False
|
|
||||||
' End Try
|
|
||||||
'End Function
|
|
||||||
'Public Sub SetMailSettings(ByVal SMTP As MailServer_Settings, ByVal MAIL As Mail_Settings)
|
|
||||||
' ArchiveMail = New EMail(SMTP, MAIL)
|
|
||||||
'End Sub
|
|
||||||
|
|
||||||
|
|
||||||
#Region "IDisposable Support"
|
|
||||||
Private disposedValue As Boolean ' To detect redundant calls
|
|
||||||
|
|
||||||
' IDisposable
|
|
||||||
Protected Overridable Sub Dispose(disposing As Boolean)
|
|
||||||
If Not disposedValue Then
|
|
||||||
If disposing Then
|
|
||||||
' TODO: dispose managed state (managed objects).
|
|
||||||
Try
|
|
||||||
If IsNothing(Me._ArchiveFileStream) = False Then Me._ArchiveFileStream.Dispose()
|
|
||||||
If IsNothing(Me.ZipOut) = False Then Me.ZipOut.Dispose()
|
|
||||||
Me.ZipIn = Nothing
|
|
||||||
Catch ex As Exception
|
|
||||||
|
|
||||||
End Try
|
|
||||||
End If
|
|
||||||
|
|
||||||
' TODO: free unmanaged resources (unmanaged objects) and override Finalize() below.
|
|
||||||
' TODO: set large fields to null.
|
|
||||||
End If
|
|
||||||
disposedValue = True
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
' TODO: override Finalize() only if Dispose(disposing As Boolean) above has code to free unmanaged resources.
|
|
||||||
'Protected Overrides Sub Finalize()
|
|
||||||
' ' Do not change this code. Put cleanup code in Dispose(disposing As Boolean) above.
|
|
||||||
' Dispose(False)
|
|
||||||
' MyBase.Finalize()
|
|
||||||
'End Sub
|
|
||||||
|
|
||||||
' This code added by Visual Basic to correctly implement the disposable pattern.
|
|
||||||
Public Sub Dispose() Implements IDisposable.Dispose
|
|
||||||
' Do not change this code. Put cleanup code in Dispose(disposing As Boolean) above.
|
|
||||||
Dispose(True)
|
|
||||||
' TODO: uncomment the following line if Finalize() is overridden above.
|
|
||||||
' GC.SuppressFinalize(Me)
|
|
||||||
End Sub
|
|
||||||
#End Region
|
|
||||||
|
|
||||||
|
|
||||||
End Class
|
|
||||||
|
|
||||||
|
|
||||||
Public Module Zipping
|
|
||||||
|
|
||||||
Public SevenZipPath As String = ""
|
|
||||||
|
|
||||||
Public Sub FastAppend(ByVal FileToZip As FileInfo, ByVal ArchiveFile As FileInfo)
|
|
||||||
If FileToZip.Exists AndAlso IsNothing(ArchiveFile) = False AndAlso ArchiveFile.Exists Then
|
|
||||||
Dim Zip As New Archive(ArchiveFile) With {
|
|
||||||
.ZipAppend = True
|
|
||||||
}
|
|
||||||
Dim FL As New List(Of String) From {
|
|
||||||
FileToZip.FullName
|
|
||||||
}
|
|
||||||
Zip.Compress(FL)
|
|
||||||
End If
|
|
||||||
End Sub
|
|
||||||
Public Sub FastAppend(ByVal FileToZip As FileInfo, ByVal TgtArchiveDirectory As DirectoryInfo, ByVal ArchiveName As String)
|
|
||||||
Dim ArchiveFile As New FileInfo(TgtArchiveDirectory.FullName & If(Strings.Right(TgtArchiveDirectory.FullName, 1) = "\", "", "\") & ArchiveName)
|
|
||||||
If ArchiveFile.Exists Then Call FastAppend(FileToZip, ArchiveFile:=ArchiveFile)
|
|
||||||
End Sub
|
|
||||||
Public Sub FastAppend(ByVal FileToZip As FileInfo, ByVal TgtArchiveDirectoryPath As String, ByVal ArchiveName As String)
|
|
||||||
Dim ArchiveFile As New FileInfo(TgtArchiveDirectoryPath & If(Strings.Right(TgtArchiveDirectoryPath, 1) = "\", "", "\") & ArchiveName)
|
|
||||||
If ArchiveFile.Exists Then Call FastAppend(FileToZip, ArchiveFile:=ArchiveFile)
|
|
||||||
End Sub
|
|
||||||
Public Function FastZip(ByVal DirectoryToZip As DirectoryInfo, Optional ByVal Append As Boolean = True) As IO.FileInfo
|
|
||||||
If DirectoryToZip.Exists Then
|
|
||||||
Dim ZipFile As New FileInfo(DirectoryToZip.Name + ".7z"), cnt As Integer = 0
|
|
||||||
If Append = False Then
|
|
||||||
Do Until ZipFile.Exists = False
|
|
||||||
cnt += 1
|
|
||||||
ZipFile = New FileInfo(DirectoryToZip.Name & "_" & CStr(cnt) & ".7z")
|
|
||||||
Loop
|
|
||||||
End If
|
|
||||||
Dim Zip As New Archive(ZipFile) With {
|
|
||||||
.ZipAppend = Append
|
|
||||||
}
|
|
||||||
Zip.Compress(New List(Of FileInfo)(DirectoryToZip.GetFiles))
|
|
||||||
Return If(ZipFile.Exists, ZipFile, Nothing)
|
|
||||||
Else
|
|
||||||
Return Nothing
|
|
||||||
End If
|
|
||||||
End Function
|
|
||||||
Public Function FastZip(ByVal FileToZip As IO.FileInfo, Optional ByVal Append As Boolean = True) As IO.FileInfo
|
|
||||||
If FileToZip.Exists Then
|
|
||||||
Dim ZipFile As New FileInfo(FileToZip.Name.Replace("." & FileToZip.Extension, "") + ".7z"), cnt As Integer = 0
|
|
||||||
If Append = False Then
|
|
||||||
Do Until ZipFile.Exists = False
|
|
||||||
cnt += 1
|
|
||||||
ZipFile = New FileInfo(FileToZip.Name.Replace("." & FileToZip.Extension, "") & "_" & CStr(cnt) & ".7z")
|
|
||||||
Loop
|
|
||||||
End If
|
|
||||||
Dim Zip As New Archive(ZipFile) With {
|
|
||||||
.ZipAppend = Append
|
|
||||||
}
|
|
||||||
Dim FL As New List(Of FileInfo) From {
|
|
||||||
FileToZip
|
|
||||||
}
|
|
||||||
Zip.Compress(FL)
|
|
||||||
Return If(ZipFile.Exists, ZipFile, Nothing)
|
|
||||||
Else
|
|
||||||
Return Nothing
|
|
||||||
End If
|
|
||||||
End Function
|
|
||||||
Public Function FastZip(ByVal TgtDirectory As String, ByVal Filename As String, Optional ByVal Append As Boolean = True) As IO.FileInfo
|
|
||||||
Return FastZip(New FileInfo(TgtDirectory & If(Strings.Right(TgtDirectory, 1) = "\", "", "\") & Filename), Append)
|
|
||||||
End Function
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
End Module
|
|
||||||
|
|
||||||
End Namespace
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Fuchs_Dataservice.exe install --autostart
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<packages>
|
|
||||||
<package id="Microsoft.AspNet.Razor" version="3.2.9" targetFramework="net48" />
|
|
||||||
<package id="Microsoft.Web.Infrastructure" version="2.0.0" targetFramework="net48" />
|
|
||||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net48" />
|
|
||||||
<package id="Squid-Box.SevenZipSharp" version="1.6.1.23" targetFramework="net48" />
|
|
||||||
<package id="System.Runtime.InteropServices.RuntimeInformation" version="4.3.0" targetFramework="net48" />
|
|
||||||
<package id="Topshelf" version="4.3.0" targetFramework="net48" />
|
|
||||||
</packages>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Fuchs_Dataservice.exe uninstall
|
|
||||||
@@ -0,0 +1,446 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Xml;
|
||||||
|
using fds;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Fuchs.Tests;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Tests for the Fuchs_DataService library (MFR ERP sync).
|
||||||
|
//
|
||||||
|
// Since Topshelf/own-config were removed and the library is hosted in-process by
|
||||||
|
// Fuchs, these tests exercise the parts that are pure/deterministic and do not
|
||||||
|
// require a live SQL Server or MFR endpoint: DATEV header/CSV/XML formatting,
|
||||||
|
// UpdateNeed parsing, the FdsShared utility helpers, config resolution, and the
|
||||||
|
// (network-free) FdsMfrClient construction.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>DATEV header line formatting — pure string assembly, no I/O.</summary>
|
||||||
|
public class DatevHeaderTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ToHeaderString_Defaults_ProducesThirtySemicolonFields()
|
||||||
|
{
|
||||||
|
var header = new DatevHeader();
|
||||||
|
var parts = header.ToHeaderString().Split(';');
|
||||||
|
|
||||||
|
Assert.Equal(30, parts.Length);
|
||||||
|
Assert.Equal("\"EXTF\"", parts[0]); // Kennzeichen quoted
|
||||||
|
Assert.Equal("700", parts[1]); // Versionsnummer
|
||||||
|
Assert.Equal("1", parts[20]); // Festschreibung true → "1"
|
||||||
|
Assert.Equal("\"EUR\"", parts[21]); // WKZ quoted
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToHeaderString_MapsNumericAndDateFields()
|
||||||
|
{
|
||||||
|
var header = new DatevHeader
|
||||||
|
{
|
||||||
|
Beraternummer = 12345,
|
||||||
|
Mandantennummer = 678,
|
||||||
|
Sachkontenlänge = 4,
|
||||||
|
WJBeginn = new DateTime(2026, 1, 1),
|
||||||
|
DatumVon = new DateTime(2026, 7, 1),
|
||||||
|
DatumBis = new DateTime(2026, 7, 31),
|
||||||
|
Bezeichnung = "fds_m260731",
|
||||||
|
};
|
||||||
|
var parts = header.ToHeaderString().Split(';');
|
||||||
|
|
||||||
|
Assert.Equal("12345", parts[10]);
|
||||||
|
Assert.Equal("678", parts[11]);
|
||||||
|
Assert.Equal("20260101", parts[12]);
|
||||||
|
Assert.Equal("4", parts[13]);
|
||||||
|
Assert.Equal("20260701", parts[14]);
|
||||||
|
Assert.Equal("20260731", parts[15]);
|
||||||
|
Assert.Equal("\"fds_m260731\"", parts[16]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true, "1")]
|
||||||
|
[InlineData(false, "0")]
|
||||||
|
public void ToHeaderString_Festschreibung_MapsToFlag(bool festschreibung, string expected)
|
||||||
|
{
|
||||||
|
var header = new DatevHeader { Festschreibung = festschreibung };
|
||||||
|
Assert.Equal(expected, header.ToHeaderString().Split(';')[20]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToHeaderString_DtvfKennzeichen_IsHonoured()
|
||||||
|
{
|
||||||
|
var header = new DatevHeader { Kennzeichen = DatevKennzeichen.DTVF };
|
||||||
|
Assert.Equal("\"DTVF\"", header.ToHeaderString().Split(';')[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(DatevFormatkategorie.Debitoren__Kreditoren, "Debitoren/Kreditoren")]
|
||||||
|
[InlineData(DatevFormatkategorie.Buchungsstapel, "Buchungsstapel")]
|
||||||
|
[InlineData(DatevFormatkategorie.Wiederkehrende_Buchungen, "Wiederkehrende Buchungen")]
|
||||||
|
public void Formatname_TranslatesEnumUnderscores(DatevFormatkategorie kat, string expected)
|
||||||
|
{
|
||||||
|
var header = new DatevHeader { Formatkategorie = kat };
|
||||||
|
Assert.Equal(expected, header.Formatname);
|
||||||
|
// Field 3 embeds the same, quoted.
|
||||||
|
Assert.Equal($"\"{expected}\"", header.ToHeaderString().Split(';')[3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>DATEV CSV + document XML generation on FdsMfr (no DB / no MFR).</summary>
|
||||||
|
public class FdsMfrDatevTests
|
||||||
|
{
|
||||||
|
private static FdsMfr NewMfr() =>
|
||||||
|
new(NullLogger<FdsMfr>.Instance, NullLoggerFactory.Instance);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DATEV_PrependsHeaderLine_AndSemicolonCsvWithColumnHeaders()
|
||||||
|
{
|
||||||
|
var header = new DatevHeader { Bezeichnung = "test" };
|
||||||
|
var tbl = new DataTable();
|
||||||
|
tbl.Columns.Add("Konto", typeof(string));
|
||||||
|
tbl.Columns.Add("Betrag", typeof(decimal));
|
||||||
|
tbl.Rows.Add("1200", 119.50m);
|
||||||
|
|
||||||
|
var result = NewMfr().DATEV(header, tbl);
|
||||||
|
var lines = result.Split("\r\n");
|
||||||
|
|
||||||
|
Assert.Equal(header.ToHeaderString(), lines[0]); // header line first
|
||||||
|
Assert.Equal("Konto;Betrag", lines[1]); // column headers, semicolon-delimited, unquoted
|
||||||
|
Assert.Contains("1200", lines[2]);
|
||||||
|
// de-DE culture → decimal comma
|
||||||
|
Assert.Contains("119,5", lines[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: only the root <archive> is in the DATEV namespace; the generator emits child
|
||||||
|
// elements in the empty namespace (existing production behavior). Child lookups therefore
|
||||||
|
// use namespace-agnostic local-name() XPath rather than the datev namespace prefix.
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateDatevDocumentXml_BuildsArchiveWithNamespaceAndOneDocumentPerInput()
|
||||||
|
{
|
||||||
|
var docs = new List<DatevDocument>
|
||||||
|
{
|
||||||
|
new("guid-1", "invoice1.pdf", "RgNr: 100", "ProcessWeb_Belege", "2026/07"),
|
||||||
|
new("guid-2", "invoice2.pdf", "", "ProcessWeb_Belege", "2026/07"),
|
||||||
|
};
|
||||||
|
|
||||||
|
var xml = NewMfr().CreateDatevDocumentXml(docs);
|
||||||
|
var doc = new XmlDocument();
|
||||||
|
doc.LoadXml(xml);
|
||||||
|
|
||||||
|
Assert.Equal("archive", doc.DocumentElement!.LocalName);
|
||||||
|
Assert.Equal("http://xml.datev.de/bedi/tps/document/v04.0", doc.DocumentElement.NamespaceURI);
|
||||||
|
Assert.Equal("ProcessWeb", doc.DocumentElement.GetAttribute("generatingSystem"));
|
||||||
|
Assert.Equal("4.0", doc.DocumentElement.GetAttribute("version"));
|
||||||
|
|
||||||
|
Assert.NotNull(doc.SelectSingleNode("//*[local-name()='header']/*[local-name()='date']"));
|
||||||
|
Assert.NotNull(doc.SelectSingleNode("//*[local-name()='header']/*[local-name()='description']"));
|
||||||
|
|
||||||
|
var documentNodes = doc.SelectNodes("//*[local-name()='content']/*[local-name()='document']");
|
||||||
|
Assert.Equal(2, documentNodes!.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateDatevDocumentXml_OmitsKeywordsWhenEmpty_IncludesWhenPresent()
|
||||||
|
{
|
||||||
|
var docs = new List<DatevDocument>
|
||||||
|
{
|
||||||
|
new("g-withkw", "a.pdf", "keyword-here", "ProcessWeb_Belege", "2026/07"),
|
||||||
|
new("g-nokw", "b.pdf", "", "ProcessWeb_Belege", "2026/07"),
|
||||||
|
};
|
||||||
|
|
||||||
|
var doc = new XmlDocument();
|
||||||
|
doc.LoadXml(NewMfr().CreateDatevDocumentXml(docs));
|
||||||
|
|
||||||
|
var withKw = doc.SelectSingleNode("//*[local-name()='document'][@guid='g-withkw']")!;
|
||||||
|
var noKw = doc.SelectSingleNode("//*[local-name()='document'][@guid='g-nokw']")!;
|
||||||
|
|
||||||
|
var withKwNode = withKw.SelectSingleNode("*[local-name()='keywords']");
|
||||||
|
Assert.NotNull(withKwNode);
|
||||||
|
Assert.Equal("keyword-here", withKwNode.InnerText);
|
||||||
|
Assert.Null(noKw.SelectSingleNode("*[local-name()='keywords']"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateDatevDocumentXml_EmitsThreeRepositoryLevels()
|
||||||
|
{
|
||||||
|
var docs = new List<DatevDocument>
|
||||||
|
{
|
||||||
|
new("g1", "a.pdf", "", "ProcessWeb_Belege", "2026/07_w03"),
|
||||||
|
};
|
||||||
|
var doc = new XmlDocument();
|
||||||
|
doc.LoadXml(NewMfr().CreateDatevDocumentXml(docs));
|
||||||
|
|
||||||
|
var levels = doc.SelectNodes(
|
||||||
|
"//*[local-name()='document']/*[local-name()='repository']/*[local-name()='level']")!;
|
||||||
|
Assert.Equal(3, levels.Count);
|
||||||
|
Assert.Equal("ProcessWeb", ((XmlElement)levels[0]!).GetAttribute("name"));
|
||||||
|
Assert.Equal("ProcessWeb_Belege", ((XmlElement)levels[1]!).GetAttribute("name"));
|
||||||
|
Assert.Equal("2026/07_w03", ((XmlElement)levels[2]!).GetAttribute("name"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>FdsMfr.UpdateNeed parsing + enum contract.</summary>
|
||||||
|
public class FdsMfrUpdateNeedTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Reset", FdsMfr.UpdateNeed.Reset)]
|
||||||
|
[InlineData("Full", FdsMfr.UpdateNeed.Full)]
|
||||||
|
[InlineData("Short", FdsMfr.UpdateNeed.Short)]
|
||||||
|
[InlineData("None", FdsMfr.UpdateNeed.None)]
|
||||||
|
public void UpdateNeedValue_ValidName_Parses(string name, FdsMfr.UpdateNeed expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, FdsMfr.UpdateNeedValue(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateNeedValue_UnknownName_Throws()
|
||||||
|
{
|
||||||
|
Assert.ThrowsAny<Exception>(() => FdsMfr.UpdateNeedValue("Bogus"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateNeed_NumericValues_AreStable()
|
||||||
|
{
|
||||||
|
// These map to SQL updateneed codes — must not drift.
|
||||||
|
Assert.Equal(5, (int)FdsMfr.UpdateNeed.Reset);
|
||||||
|
Assert.Equal(2, (int)FdsMfr.UpdateNeed.Full);
|
||||||
|
Assert.Equal(1, (int)FdsMfr.UpdateNeed.Short);
|
||||||
|
Assert.Equal(0, (int)FdsMfr.UpdateNeed.None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>FdsShared utility helpers — pure formatting + local file I/O.</summary>
|
||||||
|
public class FdsSharedTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData((byte)0)]
|
||||||
|
[InlineData((byte)1)]
|
||||||
|
[InlineData((byte)16)]
|
||||||
|
[InlineData((byte)32)]
|
||||||
|
public void RandomString_ReturnsRequestedLength_AlphanumericOnly(byte length)
|
||||||
|
{
|
||||||
|
var s = FdsShared.RandomString(length);
|
||||||
|
Assert.Equal(length, s.Length);
|
||||||
|
Assert.All(s, c => Assert.True(char.IsLetterOrDigit(c), $"unexpected char '{c}'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToCsv_DataRow_QuotesAndEscapesStringsWhenRequested()
|
||||||
|
{
|
||||||
|
var tbl = new DataTable();
|
||||||
|
tbl.Columns.Add("Name", typeof(string));
|
||||||
|
var row = tbl.NewRow();
|
||||||
|
row["Name"] = "Say \"hi\"";
|
||||||
|
tbl.Rows.Add(row);
|
||||||
|
|
||||||
|
Assert.Equal("\"Say \"\"hi\"\"\"", row.ToCsv(quoteStrings: true, CultureInfo.InvariantCulture));
|
||||||
|
Assert.Equal("Say \"hi\"", row.ToCsv(quoteStrings: false, CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToCsv_DataRow_NullAndDbNull_RenderAsEmpty()
|
||||||
|
{
|
||||||
|
var tbl = new DataTable();
|
||||||
|
tbl.Columns.Add("A", typeof(string));
|
||||||
|
tbl.Columns.Add("B", typeof(string));
|
||||||
|
var row = tbl.NewRow();
|
||||||
|
row["A"] = DBNull.Value;
|
||||||
|
row["B"] = "x";
|
||||||
|
|
||||||
|
Assert.Equal(";x", row.ToCsv(quoteStrings: false, CultureInfo.InvariantCulture, delimiter: ";"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToCsv_DataRow_FormatsDecimalWithSuppliedCulture()
|
||||||
|
{
|
||||||
|
var tbl = new DataTable();
|
||||||
|
tbl.Columns.Add("V", typeof(decimal));
|
||||||
|
var row = tbl.NewRow();
|
||||||
|
row["V"] = 1234.5m;
|
||||||
|
tbl.Rows.Add(row);
|
||||||
|
|
||||||
|
Assert.Equal("1234.5", row.ToCsv(quoteStrings: false, CultureInfo.InvariantCulture));
|
||||||
|
Assert.Equal("1234,5", row.ToCsv(quoteStrings: false, new CultureInfo("de-DE")));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToCsv_DataTable_IncludesHeaderRow_WhenRequested()
|
||||||
|
{
|
||||||
|
var tbl = new DataTable();
|
||||||
|
tbl.Columns.Add("Konto", typeof(string));
|
||||||
|
tbl.Columns.Add("Betrag", typeof(decimal));
|
||||||
|
tbl.Rows.Add("1200", 5m);
|
||||||
|
tbl.Rows.Add("1400", 6m);
|
||||||
|
|
||||||
|
var csv = tbl.ToCsv(includeHeaders: true, quoteStrings: false, fieldDelimiter: ";");
|
||||||
|
var lines = csv.Split("\r\n");
|
||||||
|
|
||||||
|
Assert.Equal("Konto;Betrag", lines[0]);
|
||||||
|
Assert.Equal("1200;5", lines[1]);
|
||||||
|
Assert.Equal("1400;6", lines[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToCsv_DataTable_CanOmitHeaders()
|
||||||
|
{
|
||||||
|
var tbl = new DataTable();
|
||||||
|
tbl.Columns.Add("X", typeof(string));
|
||||||
|
tbl.Rows.Add("a");
|
||||||
|
|
||||||
|
var csv = tbl.ToCsv(includeHeaders: false, quoteStrings: false);
|
||||||
|
Assert.Equal("a", csv);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("archive.zip", "archive")]
|
||||||
|
[InlineData("archive.tar.gz", "archive.tar")]
|
||||||
|
[InlineData("noext", "noext")]
|
||||||
|
public void NameBase_StripsFinalExtension(string fileName, string expected)
|
||||||
|
{
|
||||||
|
var fi = new FileInfo(Path.Combine(Path.GetTempPath(), fileName));
|
||||||
|
Assert.Equal(expected, fi.NameBase());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Hello, DATEV!")]
|
||||||
|
[InlineData("Ümläute & Straße")]
|
||||||
|
public void ToByteArray_Utf8_RoundTrips(string input)
|
||||||
|
{
|
||||||
|
// Default encoding is Encoding.UTF8, so the StreamWriter emits a leading BOM.
|
||||||
|
var bytes = input.ToByteArray();
|
||||||
|
var decoded = Encoding.UTF8.GetString(bytes).TrimStart('');
|
||||||
|
Assert.Equal(input, decoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToByteArray_Iso8859_1_RoundTrips()
|
||||||
|
{
|
||||||
|
const string input = "Grüße";
|
||||||
|
var latin1 = Encoding.GetEncoding("ISO-8859-1");
|
||||||
|
var bytes = input.ToByteArray(latin1);
|
||||||
|
Assert.Equal(input, latin1.GetString(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReadWriteStream_CopiesFullContent()
|
||||||
|
{
|
||||||
|
var payload = Encoding.UTF8.GetBytes("stream-copy-payload-0123456789");
|
||||||
|
using var src = new MemoryStream(payload);
|
||||||
|
using var dst = new MemoryStream();
|
||||||
|
|
||||||
|
Assert.True(FdsShared.ReadWriteStream(src, dst, closeWriteStream: false));
|
||||||
|
Assert.Equal(payload, dst.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WriteStreamToDisk_PersistsStreamToFile()
|
||||||
|
{
|
||||||
|
var payload = Encoding.UTF8.GetBytes("disk-payload");
|
||||||
|
var path = Path.Combine(Path.GetTempPath(), $"fds_test_{Guid.NewGuid():N}.bin");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var src = new MemoryStream(payload);
|
||||||
|
Assert.True(FdsShared.WriteStreamToDisk(src, path));
|
||||||
|
Assert.True(File.Exists(path));
|
||||||
|
Assert.Equal(payload, File.ReadAllBytes(path));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (File.Exists(path)) File.Delete(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// FdsConfig + FdsMfrClient share process-global config state (FdsConfig._config),
|
||||||
|
/// so their tests are grouped into one collection to run sequentially and never
|
||||||
|
/// clobber each other's <see cref="FdsConfig.Initialize"/> call.
|
||||||
|
/// </summary>
|
||||||
|
[CollectionDefinition("FdsConfig")]
|
||||||
|
public class FdsConfigCollection { }
|
||||||
|
|
||||||
|
[Collection("FdsConfig")]
|
||||||
|
public class FdsConfigTests
|
||||||
|
{
|
||||||
|
private static IConfiguration BuildConfig(Dictionary<string, string?> values) =>
|
||||||
|
new ConfigurationBuilder().AddInMemoryCollection(values).Build();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FDSConnectionString_ReturnsConfiguredValue()
|
||||||
|
{
|
||||||
|
FdsConfig.Initialize(BuildConfig(new()
|
||||||
|
{
|
||||||
|
["ConnectionStrings:fuchs_fds_ConnectionString"] = "Server=.;Database=fds;",
|
||||||
|
}));
|
||||||
|
Assert.Equal("Server=.;Database=fds;", FdsConfig.FDSConnectionString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FDSConnectionString_Missing_Throws()
|
||||||
|
{
|
||||||
|
FdsConfig.Initialize(BuildConfig(new()));
|
||||||
|
Assert.Throws<InvalidOperationException>(() => FdsConfig.FDSConnectionString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MfrSettings_ResolveFromFdsSection()
|
||||||
|
{
|
||||||
|
FdsConfig.Initialize(BuildConfig(new()
|
||||||
|
{
|
||||||
|
["Fds:MFR_UserName"] = "system@example.com",
|
||||||
|
["Fds:MFR_Password"] = "secret",
|
||||||
|
["Fds:MFR_host"] = "portal.mobilefieldreport.com",
|
||||||
|
}));
|
||||||
|
|
||||||
|
Assert.Equal("system@example.com", FdsConfig.MFR_UserName);
|
||||||
|
Assert.Equal("secret", FdsConfig.MFR_Password);
|
||||||
|
Assert.Equal("portal.mobilefieldreport.com", FdsConfig.MFR_host);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MfrSettings_Absent_FallBackToEmptyString()
|
||||||
|
{
|
||||||
|
FdsConfig.Initialize(BuildConfig(new()));
|
||||||
|
Assert.Equal("", FdsConfig.MFR_UserName);
|
||||||
|
Assert.Equal("", FdsConfig.MFR_Password);
|
||||||
|
Assert.Equal("", FdsConfig.MFR_host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Collection("FdsConfig")]
|
||||||
|
public class FdsMfrClientTests
|
||||||
|
{
|
||||||
|
private static void InitConfig(string host) =>
|
||||||
|
FdsConfig.Initialize(new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
["Fds:MFR_host"] = host,
|
||||||
|
["Fds:MFR_UserName"] = "u",
|
||||||
|
["Fds:MFR_Password"] = "p",
|
||||||
|
}).Build());
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Construction_BuildsClientConfig_FromHost()
|
||||||
|
{
|
||||||
|
InitConfig("portal.mobilefieldreport.com");
|
||||||
|
using var client = new FdsMfrClient();
|
||||||
|
Assert.Contains("portal.mobilefieldreport.com", client.ClientConfig.BaseUrl);
|
||||||
|
Assert.EndsWith("/", client.ClientConfig.BaseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsReadonly_DefaultsTrue_AndIsSettable()
|
||||||
|
{
|
||||||
|
InitConfig("portal.mobilefieldreport.com");
|
||||||
|
using var client = new FdsMfrClient();
|
||||||
|
Assert.True(client.IsReadonly);
|
||||||
|
client.IsReadonly = false;
|
||||||
|
Assert.False(client.IsReadonly);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
|
||||||
<PackageReference Include="xunit.v3" Version="3.2.2" />
|
<PackageReference Include="xunit.v3" Version="3.2.2" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Fuchs.intranet;
|
using Fuchs.intranet;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@@ -22,6 +22,88 @@ public class InvoiceDraftCalculatorTests
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── RecomputeLineValues ────────────────────────────────────────────────────
|
||||||
|
private static JObject Line(InvoiceDraftSession s, int block, int line) =>
|
||||||
|
(JObject)((JArray)((JObject)s.Req[block])["itm"]!)[line];
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RecomputeLineValues_ComputesNetAndVatFromRawQtyPriceAndRate()
|
||||||
|
{
|
||||||
|
var s = SessionWith(@"[
|
||||||
|
{ 'Id':'10','itm':[ {'id':'a','typ':'material','qn':2,'v':50,'vat':'19%'} ] }
|
||||||
|
]");
|
||||||
|
|
||||||
|
InvoiceDraftCalculator.RecomputeLineValues(s);
|
||||||
|
|
||||||
|
var l = Line(s, 0, 0);
|
||||||
|
Assert.Equal(100m, l["vt"]!.Value<decimal>());
|
||||||
|
Assert.Equal(19m, l["vv"]!.Value<decimal>());
|
||||||
|
Assert.Equal(0m, l["vs"]!.Value<decimal>());
|
||||||
|
Assert.Equal(0m, l["vsv"]!.Value<decimal>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RecomputeLineValues_ServiceType_AlsoFillsServiceNetAndVat()
|
||||||
|
{
|
||||||
|
var s = SessionWith(@"[
|
||||||
|
{ 'Id':'10','itm':[ {'id':'a','typ':'Service','qn':3,'v':10,'vat':'19%'} ] }
|
||||||
|
]");
|
||||||
|
|
||||||
|
InvoiceDraftCalculator.RecomputeLineValues(s);
|
||||||
|
|
||||||
|
var l = Line(s, 0, 0);
|
||||||
|
Assert.Equal(30m, l["vt"]!.Value<decimal>());
|
||||||
|
Assert.Equal(5.7m, l["vv"]!.Value<decimal>());
|
||||||
|
Assert.Equal(30m, l["vs"]!.Value<decimal>());
|
||||||
|
Assert.Equal(5.7m, l["vsv"]!.Value<decimal>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0, 50)] // no quantity posted -> leave value untouched
|
||||||
|
[InlineData(2, 0)] // no price posted -> leave value untouched
|
||||||
|
public void RecomputeLineValues_MissingQtyOrPrice_LeavesExistingValueUntouched(decimal qty, decimal price)
|
||||||
|
{
|
||||||
|
var s = SessionWith($@"[
|
||||||
|
{{ 'Id':'10','itm':[ {{'id':'a','typ':'material','qn':{qty},'v':{price},'vat':'19%','vt':777,'vv':111}} ] }}
|
||||||
|
]");
|
||||||
|
|
||||||
|
InvoiceDraftCalculator.RecomputeLineValues(s);
|
||||||
|
|
||||||
|
var l = Line(s, 0, 0);
|
||||||
|
Assert.Equal(777m, l["vt"]!.Value<decimal>()); // untouched — mirrors quantChange's own guard
|
||||||
|
Assert.Equal(111m, l["vv"]!.Value<decimal>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RecomputeLineValues_ZeroVatRate_ComputesNetWithNoVat()
|
||||||
|
{
|
||||||
|
var s = SessionWith(@"[
|
||||||
|
{ 'Id':'10','itm':[ {'id':'a','typ':'material','qn':4,'v':25,'vat':'0%'} ] }
|
||||||
|
]");
|
||||||
|
|
||||||
|
InvoiceDraftCalculator.RecomputeLineValues(s);
|
||||||
|
|
||||||
|
var l = Line(s, 0, 0);
|
||||||
|
Assert.Equal(100m, l["vt"]!.Value<decimal>());
|
||||||
|
Assert.Equal(0m, l["vv"]!.Value<decimal>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RecomputeLineValues_SetHeaderConvertedSum_IsNotClobberedByRecompute()
|
||||||
|
{
|
||||||
|
// A converted set header carries a synthesised sum (no raw qty/price of its own) —
|
||||||
|
// RecomputeLineValues must never overwrite it, mirroring quantChange's guard.
|
||||||
|
var s = SessionWith(@"[
|
||||||
|
{ 'Id':'10','itm':[ {'id':'h','typ':'set','vt':1000,'vv':190,'vat':'19%'} ] }
|
||||||
|
]");
|
||||||
|
|
||||||
|
InvoiceDraftCalculator.RecomputeLineValues(s);
|
||||||
|
|
||||||
|
var l = Line(s, 0, 0);
|
||||||
|
Assert.Equal(1000m, l["vt"]!.Value<decimal>());
|
||||||
|
Assert.Equal(190m, l["vv"]!.Value<decimal>());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RecomputeTotals_SumsNetVatServiceAndPerBlock()
|
public void RecomputeTotals_SumsNetVatServiceAndPerBlock()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Fuchs.intranet;
|
using Fuchs.intranet;
|
||||||
using Fuchs.Services;
|
using Fuchs.Services;
|
||||||
@@ -205,9 +205,9 @@ public class InvoiceDraftServiceTests
|
|||||||
var (svc, _, _) = NewService();
|
var (svc, _, _) = NewService();
|
||||||
var s = svc.OpenFromPayload(Payload(), "user1");
|
var s = svc.OpenFromPayload(Payload(), "user1");
|
||||||
|
|
||||||
var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "setmode", Value = JToken.FromObject("itemprices") });
|
var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "setmode", Value = JToken.FromObject("setonly") });
|
||||||
|
|
||||||
Assert.Equal("itemprices", s2!.Admin["setmode"]!.Value<string>());
|
Assert.Equal("setonly", s2!.Admin["setmode"]!.Value<string>());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -240,6 +240,249 @@ public class InvoiceDraftServiceTests
|
|||||||
Assert.Equal(30m, s2.Sums.NetByBlock["2"]);
|
Assert.Equal(30m, s2.Sums.NetByBlock["2"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fds__prepInvoice's [SetItmID] window function anchors on the still-unconverted (price 0)
|
||||||
|
// Set header: the header row's own SetItmId self-references its own id ('1'), never null —
|
||||||
|
// ApplyItemSetPrice must still recognize id-equality first and never treat the header as its
|
||||||
|
// own member (see ApplyItemSetPrice's id == Ref check).
|
||||||
|
private static JObject SetPayload() => JObject.Parse(@"{
|
||||||
|
'admin':{'p13b':false,'type':'r','paymentterms':'10wd'},
|
||||||
|
'new':{'invoiceemail':'a@b.de','invoiceaddress':'Weg 1','invoicetitle':'Rechnung'},
|
||||||
|
'req':[{'Id':'1','text':'Auftrag','itm':[
|
||||||
|
{'id':'1','typ':'set','vt':0,'vv':0,'vs':0,'vsv':0,'vat':'19%','SetItmId':'1'},
|
||||||
|
{'id':'2','typ':'material','vt':60,'vv':11.4,'vs':0,'vsv':0,'vat':'19%','SetItmId':'1'},
|
||||||
|
{'id':'3','typ':'material','vt':40,'vv':7.6,'vs':10,'vsv':1.9,'vat':'19%','SetItmId':'1'}],
|
||||||
|
'items':[{'id':'1','type':'set'},{'id':'2','type':'material','setId':'1','total_net':60,'vat':'19%'},
|
||||||
|
{'id':'3','type':'material','setId':'1','total_net':40,'vat':'19%'}]}]
|
||||||
|
}");
|
||||||
|
|
||||||
|
// A fourth, unrelated item ('4') follows the set's members in the same block but was never
|
||||||
|
// attributed a SetItmId by fds__prepInvoice (it isn't part of the set) — it must stay untouched
|
||||||
|
// by the conversion, proving membership is driven purely by SetItmId, not row order/position.
|
||||||
|
private static JObject SetPayloadWithTrailingUnrelatedItem() => JObject.Parse(@"{
|
||||||
|
'admin':{'p13b':false,'type':'r','paymentterms':'10wd'},
|
||||||
|
'new':{'invoiceemail':'a@b.de','invoiceaddress':'Weg 1','invoicetitle':'Rechnung'},
|
||||||
|
'req':[{'Id':'1','text':'Auftrag','itm':[
|
||||||
|
{'id':'1','typ':'set','vt':0,'vv':0,'vs':0,'vsv':0,'vat':'19%','SetItmId':'1'},
|
||||||
|
{'id':'2','typ':'material','vt':60,'vv':11.4,'vs':0,'vsv':0,'vat':'19%','SetItmId':'1'},
|
||||||
|
{'id':'3','typ':'material','vt':40,'vv':7.6,'vs':10,'vsv':1.9,'vat':'19%','SetItmId':'1'},
|
||||||
|
{'id':'4','typ':'material','vt':25,'vv':4.75,'vs':0,'vsv':0,'vat':'19%','SetItmId':null}],
|
||||||
|
'items':[{'id':'1','type':'set'},{'id':'2','type':'material','setId':'1','total_net':60,'vat':'19%'},
|
||||||
|
{'id':'3','type':'material','setId':'1','total_net':40,'vat':'19%'},
|
||||||
|
{'id':'4','type':'material','total_net':25,'vat':'19%'}]}]
|
||||||
|
}");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApplyPatch_ItemSetPrice_SumsMembersOntoHeaderAndNullsMembers()
|
||||||
|
{
|
||||||
|
var (svc, _, _) = NewService();
|
||||||
|
var s = svc.OpenFromPayload(SetPayload(), "user1");
|
||||||
|
|
||||||
|
var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "item.setprice", Ref = "1" });
|
||||||
|
|
||||||
|
Assert.NotNull(s2);
|
||||||
|
Assert.Equal(1, s2!.Version);
|
||||||
|
var block = (JObject)s2.Req[0];
|
||||||
|
var lines = (JArray)block["itm"]!;
|
||||||
|
var header = lines.OfType<JObject>().Single(l => (string)l["id"]! == "1");
|
||||||
|
var m2 = lines.OfType<JObject>().Single(l => (string)l["id"]! == "2");
|
||||||
|
var m3 = lines.OfType<JObject>().Single(l => (string)l["id"]! == "3");
|
||||||
|
|
||||||
|
Assert.Equal(100m, header["vt"]!.Value<decimal>()); // 60 + 40
|
||||||
|
Assert.Equal(19m, header["vv"]!.Value<decimal>()); // 11.4 + 7.6
|
||||||
|
Assert.Equal(10m, header["vs"]!.Value<decimal>());
|
||||||
|
Assert.Equal(1.9m, header["vsv"]!.Value<decimal>());
|
||||||
|
Assert.Equal(JTokenType.Null, m2["vt"]!.Type); // ADR 0009: members nulled (empty cell), not 0
|
||||||
|
Assert.Equal(JTokenType.Null, m2["vv"]!.Type);
|
||||||
|
Assert.Equal(JTokenType.Null, m3["vt"]!.Type);
|
||||||
|
Assert.Equal(JTokenType.Null, m3["vv"]!.Type);
|
||||||
|
|
||||||
|
// Total invoice sum is unchanged by the conversion (set price == sum of members).
|
||||||
|
Assert.Equal(100m, s2.Sums.TotalNet);
|
||||||
|
Assert.Equal(19m, s2.Sums.TotalVat);
|
||||||
|
|
||||||
|
var h = Assert.Single(s2.History);
|
||||||
|
Assert.Equal("item.setprice", h.Target);
|
||||||
|
Assert.Equal("1", h.Ref);
|
||||||
|
Assert.Equal("0", h.OldValue);
|
||||||
|
Assert.Equal("100", h.NewValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApplyPatch_ItemSetPrice_UnknownRef_IsNoOp()
|
||||||
|
{
|
||||||
|
var (svc, _, _) = NewService();
|
||||||
|
var s = svc.OpenFromPayload(SetPayload(), "user1");
|
||||||
|
|
||||||
|
var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "item.setprice", Ref = "999" });
|
||||||
|
|
||||||
|
Assert.NotNull(s2);
|
||||||
|
Assert.Equal(0, s2!.Version);
|
||||||
|
Assert.Empty(s2.History);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApplyPatch_ItemSetPrice_RefNotASetHeader_IsNoOp()
|
||||||
|
{
|
||||||
|
var (svc, _, _) = NewService();
|
||||||
|
var s = svc.OpenFromPayload(SetPayload(), "user1");
|
||||||
|
|
||||||
|
var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "item.setprice", Ref = "2" });
|
||||||
|
|
||||||
|
Assert.NotNull(s2);
|
||||||
|
Assert.Equal(0, s2!.Version);
|
||||||
|
Assert.Empty(s2.History);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApplyPatch_ItemSetPrice_HeaderSelfReferencingSetItmId_NeverCountsHeaderAsOwnMember()
|
||||||
|
{
|
||||||
|
// Regression test: fds__prepInvoice no longer nulls out the header's own SetItmId (it
|
||||||
|
// self-references its own id). ApplyItemSetPrice must still sum exactly the two real
|
||||||
|
// members (100 net), not 3x by also including the header as if it were a member of itself.
|
||||||
|
var (svc, _, _) = NewService();
|
||||||
|
var s = svc.OpenFromPayload(SetPayload(), "user1");
|
||||||
|
|
||||||
|
var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "item.setprice", Ref = "1" });
|
||||||
|
|
||||||
|
Assert.NotNull(s2);
|
||||||
|
var block = (JObject)s2!.Req[0];
|
||||||
|
var lines = (JArray)block["itm"]!;
|
||||||
|
var header = lines.OfType<JObject>().Single(l => (string)l["id"]! == "1");
|
||||||
|
|
||||||
|
Assert.Equal(100m, header["vt"]!.Value<decimal>()); // 60 + 40, not tripled by self-inclusion
|
||||||
|
Assert.Equal(100m, s2.Sums.TotalNet);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApplyPatch_ItemSetPrice_UnrelatedItemAfterMembers_IsNeverSweptIntoSet()
|
||||||
|
{
|
||||||
|
// Regression test for the reported bug: an item after the set's real members in the same
|
||||||
|
// block, but with no SetItmId of its own, must stay fully priced and untouched — only
|
||||||
|
// items the server actually tagged with SetItmId == the header id are members.
|
||||||
|
var (svc, _, _) = NewService();
|
||||||
|
var s = svc.OpenFromPayload(SetPayloadWithTrailingUnrelatedItem(), "user1");
|
||||||
|
|
||||||
|
var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "item.setprice", Ref = "1" });
|
||||||
|
|
||||||
|
Assert.NotNull(s2);
|
||||||
|
var block = (JObject)s2!.Req[0];
|
||||||
|
var lines = (JArray)block["itm"]!;
|
||||||
|
var header = lines.OfType<JObject>().Single(l => (string)l["id"]! == "1");
|
||||||
|
var m2 = lines.OfType<JObject>().Single(l => (string)l["id"]! == "2");
|
||||||
|
var m3 = lines.OfType<JObject>().Single(l => (string)l["id"]! == "3");
|
||||||
|
var other = lines.OfType<JObject>().Single(l => (string)l["id"]! == "4");
|
||||||
|
|
||||||
|
Assert.Equal(100m, header["vt"]!.Value<decimal>()); // only the real members (60 + 40)
|
||||||
|
Assert.Equal(JTokenType.Null, m2["vt"]!.Type); // ADR 0009: nulled, not 0
|
||||||
|
Assert.Equal(JTokenType.Null, m3["vt"]!.Type);
|
||||||
|
Assert.Equal(25m, other["vt"]!.Value<decimal>()); // untouched — never part of the set
|
||||||
|
Assert.Equal(4.75m, other["vv"]!.Value<decimal>());
|
||||||
|
|
||||||
|
// Total invoice sum unaffected: 100 (set) + 25 (unrelated item) = 125.
|
||||||
|
Assert.Equal(125m, s2.Sums.TotalNet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Block set-price menu modes (ADR 0009): per-service-request-block, irreversible ──
|
||||||
|
private static JObject BlocksPayload() => JObject.Parse(@"{
|
||||||
|
'admin':{'p13b':false,'type':'r'},
|
||||||
|
'new':{'invoiceemail':'a@b.de','invoiceaddress':'Weg 1','invoicetitle':'Rechnung'},
|
||||||
|
'req':[
|
||||||
|
{'Id':'1','text':'Auftrag A','itm':[
|
||||||
|
{'id':'11','typ':'material','vt':60,'vv':11.4,'vs':0,'vsv':0,'vat':'19%'},
|
||||||
|
{'id':'12','typ':'service','vt':40,'vv':7.6,'vs':40,'vsv':7.6,'vat':'19%'}],
|
||||||
|
'items':[{'id':'11','type':'material','total_net':60,'vat':'19%'},
|
||||||
|
{'id':'12','type':'service','total_net':40,'vat':'19%'}]},
|
||||||
|
{'Id':'2','text':'Auftrag B','itm':[
|
||||||
|
{'id':'21','typ':'material','vt':30,'vv':5.7,'vs':0,'vsv':0,'vat':'19%'}],
|
||||||
|
'items':[{'id':'21','type':'material','total_net':30,'vat':'19%'}]}]
|
||||||
|
}");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApplyPatch_BlockSetPrice_InsertsSetRowPerBlock_NullsMembers_TotalUnchanged()
|
||||||
|
{
|
||||||
|
var (svc, _, _) = NewService();
|
||||||
|
var s = svc.OpenFromPayload(BlocksPayload(), "user1");
|
||||||
|
Assert.Equal(130m, s.Sums.TotalNet); // 100 (A) + 30 (B)
|
||||||
|
|
||||||
|
var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "block.setprice" });
|
||||||
|
|
||||||
|
Assert.NotNull(s2);
|
||||||
|
Assert.Equal(1, s2!.Version);
|
||||||
|
|
||||||
|
var linesA = (JArray)((JObject)s2.Req[0])["itm"]!;
|
||||||
|
Assert.Equal(3, linesA.Count); // set row + the two (nulled) members, kept
|
||||||
|
var setA = (JObject)linesA[0];
|
||||||
|
Assert.Equal("set", (string)setA["typ"]!);
|
||||||
|
Assert.Equal("bset_1", (string)setA["id"]!);
|
||||||
|
Assert.Equal(100m, setA["vt"]!.Value<decimal>()); // block sum
|
||||||
|
Assert.Equal(19m, setA["vv"]!.Value<decimal>());
|
||||||
|
Assert.Equal(40m, setA["vs"]!.Value<decimal>()); // service-net split preserved
|
||||||
|
Assert.Equal(7.6m, setA["vsv"]!.Value<decimal>());
|
||||||
|
var m11 = linesA.OfType<JObject>().Single(l => (string)l["id"]! == "11");
|
||||||
|
Assert.Equal(JTokenType.Null, m11["vt"]!.Type); // ADR 0009: null (empty cell), not 0
|
||||||
|
Assert.Equal(JTokenType.Null, m11["vv"]!.Type);
|
||||||
|
|
||||||
|
// items contract mirrors it (PDF reads total_net from here).
|
||||||
|
var itemsA = (JArray)((JObject)s2.Req[0])["items"]!;
|
||||||
|
Assert.Equal("set", (string)((JObject)itemsA[0])["type"]!);
|
||||||
|
Assert.Equal(100m, ((JObject)itemsA[0])["total_net"]!.Value<decimal>());
|
||||||
|
Assert.Equal(JTokenType.Null, itemsA.OfType<JObject>().Single(i => (string)i["id"]! == "11")["total_net"]!.Type);
|
||||||
|
|
||||||
|
// second block converted too; total conserved.
|
||||||
|
Assert.Equal(30m, ((JObject)((JArray)((JObject)s2.Req[1])["itm"]!)[0])["vt"]!.Value<decimal>());
|
||||||
|
Assert.Equal(130m, s2.Sums.TotalNet);
|
||||||
|
Assert.Equal(24.7m, s2.Sums.TotalVat);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApplyPatch_BlockSetOnly_InsertsSetRowPerBlock_RemovesMembers_TotalUnchanged()
|
||||||
|
{
|
||||||
|
var (svc, _, _) = NewService();
|
||||||
|
var s = svc.OpenFromPayload(BlocksPayload(), "user1");
|
||||||
|
|
||||||
|
var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "block.setonly" });
|
||||||
|
|
||||||
|
Assert.NotNull(s2);
|
||||||
|
var linesA = (JArray)((JObject)s2!.Req[0])["itm"]!;
|
||||||
|
Assert.Single(linesA); // members removed, only the set row remains
|
||||||
|
Assert.Equal("set", (string)((JObject)linesA[0])["typ"]!);
|
||||||
|
Assert.Equal(100m, ((JObject)linesA[0])["vt"]!.Value<decimal>());
|
||||||
|
Assert.Single((JArray)((JObject)s2.Req[0])["items"]!);
|
||||||
|
Assert.Equal(130m, s2.Sums.TotalNet); // total unchanged
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApplyPatch_BlockSetPrice_SetRowNotSweptIntoBuildSetDisplay()
|
||||||
|
{
|
||||||
|
// The block set row has no setId members -> HasSetMembers is false -> it renders flat
|
||||||
|
// (emphasised on its own, members null to blank), so BuildSetDisplay must not emit flags for it.
|
||||||
|
var (svc, _, _) = NewService();
|
||||||
|
var s = svc.OpenFromPayload(BlocksPayload(), "user1");
|
||||||
|
svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "block.setprice" });
|
||||||
|
|
||||||
|
var state = JObject.FromObject(svc.BuildState(svc.Get(s.Token)!));
|
||||||
|
var setDisplay = (JObject)state["setDisplay"]!;
|
||||||
|
Assert.False(setDisplay.ContainsKey("bset_1"));
|
||||||
|
Assert.Empty(setDisplay);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApplyPatch_BlockSetPrice_EmptyOrUnpricedBlocks_IsNoOp()
|
||||||
|
{
|
||||||
|
var (svc, _, _) = NewService();
|
||||||
|
var s = svc.OpenFromPayload(JObject.Parse(@"{
|
||||||
|
'admin':{'p13b':false,'type':'r'},
|
||||||
|
'new':{'invoiceemail':'a@b.de','invoiceaddress':'Weg 1','invoicetitle':'Rechnung'},
|
||||||
|
'req':[{'Id':'1','text':'Leer','itm':[],'items':[]}]
|
||||||
|
}"), "user1");
|
||||||
|
|
||||||
|
var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "block.setprice" });
|
||||||
|
|
||||||
|
Assert.NotNull(s2);
|
||||||
|
Assert.Equal(0, s2!.Version); // nothing priced -> no-op
|
||||||
|
Assert.Empty(s2.History);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ApplyPatch_UnknownTarget_IsNoOp_NoVersionBumpNoHistory()
|
public void ApplyPatch_UnknownTarget_IsNoOp_NoVersionBumpNoHistory()
|
||||||
{
|
{
|
||||||
@@ -306,7 +549,7 @@ public class InvoiceDraftServiceTests
|
|||||||
var (svc, inv, _) = NewService();
|
var (svc, inv, _) = NewService();
|
||||||
var payload = Payload();
|
var payload = Payload();
|
||||||
payload["admin"]!["p13b"] = true;
|
payload["admin"]!["p13b"] = true;
|
||||||
payload["admin"]!["setmode"] = "itemprices";
|
payload["admin"]!["setmode"] = "setonly";
|
||||||
var s = svc.OpenFromPayload(payload, "user1");
|
var s = svc.OpenFromPayload(payload, "user1");
|
||||||
|
|
||||||
await svc.FlushToDbAsync(s.Token, "user1", null!);
|
await svc.FlushToDbAsync(s.Token, "user1", null!);
|
||||||
@@ -314,7 +557,7 @@ public class InvoiceDraftServiceTests
|
|||||||
var options = inv.Registered!.BuildInvoiceParams(change: false, invId: "")
|
var options = inv.Registered!.BuildInvoiceParams(change: false, invId: "")
|
||||||
.First(p => p.ParameterName == "@InvoiceOptions").Value?.ToString() ?? "";
|
.First(p => p.ParameterName == "@InvoiceOptions").Value?.ToString() ?? "";
|
||||||
Assert.Contains("§13b", options);
|
Assert.Contains("§13b", options);
|
||||||
Assert.Contains("setmode:itemprices", options);
|
Assert.Contains("setmode:setonly", options);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -476,4 +719,124 @@ public class InvoiceDraftServiceTests
|
|||||||
|
|
||||||
Assert.Equal(new[] { "2", "1" }, s2!.Req.Select(b => b["Id"]!.Value<string>()).ToArray());
|
Assert.Equal(new[] { "2", "1" }, s2!.Req.Select(b => b["Id"]!.Value<string>()).ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Multiple VAT rates aggregate independently (ADR 0008: server owns every tax sum) ──
|
||||||
|
private static JObject MultiRatePayload() => JObject.Parse(@"{
|
||||||
|
'admin':{'p13b':false,'type':'r'},
|
||||||
|
'new':{'invoiceemail':'a@b.de','invoiceaddress':'Weg 1'},
|
||||||
|
'req':[
|
||||||
|
{'Id':'1','text':'A','itm':[
|
||||||
|
{'id':'900','typ':'material','vt':100,'vv':19,'vat':'19%'},
|
||||||
|
{'id':'901','typ':'material','vt':200,'vv':14,'vat':'7%'},
|
||||||
|
{'id':'902','typ':'material','vt':50,'vv':0,'vat':'0%'}],
|
||||||
|
'items':[{'id':'900','type':'material','total_net':100,'vat':'19%'},
|
||||||
|
{'id':'901','type':'material','total_net':200,'vat':'7%'},
|
||||||
|
{'id':'902','type':'material','total_net':50,'vat':'0%'}]}
|
||||||
|
]}");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OpenFromPayload_MultipleVatRates_GroupsSumsPerRate()
|
||||||
|
{
|
||||||
|
var (svc, _, _) = NewService();
|
||||||
|
var s = svc.OpenFromPayload(MultiRatePayload(), "user1");
|
||||||
|
|
||||||
|
Assert.Equal(350m, s.Sums.TotalNet);
|
||||||
|
Assert.Equal(33m, s.Sums.TotalVat);
|
||||||
|
Assert.Equal(383m, s.Sums.TotalGross);
|
||||||
|
Assert.Equal(19m, s.Sums.VatByRate["19"]);
|
||||||
|
Assert.Equal(14m, s.Sums.VatByRate["7"]);
|
||||||
|
Assert.False(s.Sums.VatByRate.ContainsKey("0")); // zero-rate contributes no VAT key (mirrors calculator)
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApplyPatch_BlockReplace_MultiRate_RecomputesEachRateIndependently()
|
||||||
|
{
|
||||||
|
var (svc, _, _) = NewService();
|
||||||
|
var s = svc.OpenFromPayload(MultiRatePayload(), "user1");
|
||||||
|
|
||||||
|
// Halve the 7%-rate line's net/VAT via a block replace; the 19% bucket must stay untouched.
|
||||||
|
var newBlock = JObject.Parse(@"{'Id':'1','text':'A','itm':[
|
||||||
|
{'id':'900','typ':'material','vt':100,'vv':19,'vat':'19%'},
|
||||||
|
{'id':'901','typ':'material','vt':100,'vv':7,'vat':'7%'},
|
||||||
|
{'id':'902','typ':'material','vt':50,'vv':0,'vat':'0%'}],
|
||||||
|
'items':[]}");
|
||||||
|
var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "block.replace", Ref = "1", Value = newBlock });
|
||||||
|
|
||||||
|
Assert.Equal(19m, s2!.Sums.VatByRate["19"]); // unchanged
|
||||||
|
Assert.Equal(7m, s2.Sums.VatByRate["7"]); // recomputed from new line
|
||||||
|
Assert.Equal(250m, s2.Sums.TotalNet);
|
||||||
|
Assert.Equal(26m, s2.Sums.TotalVat);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── BuildSetDisplay reflects both set-pricing modes (mirrors the PDF's InvoiceSetPricing) ──
|
||||||
|
// SetPayload()'s header item is still unconverted (own total_net == 0, as delivered by
|
||||||
|
// fds__prepInvoice) — until the set-item switch (ApplyItemSetPrice) actually gives it its own
|
||||||
|
// price, InvoiceSetPricing.Build must not apply the chosen setmode yet: header stays blank and
|
||||||
|
// each member keeps showing its own individual price.
|
||||||
|
[Theory]
|
||||||
|
[InlineData("setprice")]
|
||||||
|
[InlineData("setonly")]
|
||||||
|
public void BuildState_SetDisplay_UnconvertedSet_HeaderBlankMembersIndividuallyPriced(string setmode)
|
||||||
|
{
|
||||||
|
var (svc, _, _) = NewService();
|
||||||
|
var payload = SetPayload();
|
||||||
|
payload["admin"]!["setmode"] = setmode;
|
||||||
|
var s = svc.OpenFromPayload(payload, "user1");
|
||||||
|
|
||||||
|
var state = JObject.FromObject(svc.BuildState(s));
|
||||||
|
var setDisplay = (JObject)state["setDisplay"]!;
|
||||||
|
|
||||||
|
Assert.False(setDisplay["1"]!["p"]!.Value<bool>()); // header not priced yet
|
||||||
|
Assert.True(setDisplay["2"]!["p"]!.Value<bool>()); // member 2 keeps its own price
|
||||||
|
Assert.True(setDisplay["3"]!["p"]!.Value<bool>()); // member 3 keeps its own price
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once converted (ApplyItemSetPrice has given the header its own price), the chosen setmode
|
||||||
|
// takes effect: SetPrice blanks the members (still present in the map); SetOnly drops them
|
||||||
|
// from the map entirely.
|
||||||
|
[Theory]
|
||||||
|
[InlineData("setprice", true, false)] // header priced, member 2 blank
|
||||||
|
[InlineData("setonly", true, false)] // header priced; member 2 dropped entirely (absent from map)
|
||||||
|
public void BuildState_SetDisplay_ConvertedSet_ReflectsSetmodeForHeaderAndMember(string setmode, bool headerShown, bool memberShown)
|
||||||
|
{
|
||||||
|
var (svc, _, _) = NewService();
|
||||||
|
var payload = SetPayload();
|
||||||
|
payload["admin"]!["setmode"] = setmode;
|
||||||
|
var s = svc.OpenFromPayload(payload, "user1");
|
||||||
|
svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "item.setprice", Ref = "1" }); // convert the set first
|
||||||
|
|
||||||
|
var state = JObject.FromObject(svc.BuildState(s));
|
||||||
|
var setDisplay = (JObject)state["setDisplay"]!;
|
||||||
|
|
||||||
|
Assert.Equal(headerShown, setDisplay["1"]!["p"]!.Value<bool>());
|
||||||
|
if (setmode == "setonly")
|
||||||
|
Assert.False(setDisplay.ContainsKey("2")); // member removed from the display entirely
|
||||||
|
else
|
||||||
|
Assert.Equal(memberShown, setDisplay["2"]!["p"]!.Value<bool>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Full lifecycle recalculation: several edits of different kinds land in one consistent recompute ──
|
||||||
|
[Fact]
|
||||||
|
public void ApplyPatch_FullEditSequence_TextReorderSetPriceAndTaxToggle_EndsConsistent()
|
||||||
|
{
|
||||||
|
var (svc, _, _) = NewService();
|
||||||
|
var s = svc.OpenFromPayload(TwoBlockPayload(), "user1");
|
||||||
|
|
||||||
|
// 1) text change
|
||||||
|
svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "title", Value = JToken.FromObject("Endabrechnung") });
|
||||||
|
// 2) reorder sections
|
||||||
|
svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "block.order", Value = JArray.Parse("['2','1']") });
|
||||||
|
// 3) toggle reverse-charge on, then off again (settings roundtrip)
|
||||||
|
svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "p13b", Value = JToken.FromObject(true) });
|
||||||
|
var final = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "p13b", Value = JToken.FromObject(false) });
|
||||||
|
|
||||||
|
Assert.NotNull(final);
|
||||||
|
Assert.Equal("Endabrechnung", final!.New["invoicetitle"]!.Value<string>());
|
||||||
|
Assert.Equal(new[] { "2", "1" }, final.Req.Select(b => b["Id"]!.Value<string>()).ToArray());
|
||||||
|
Assert.Equal("1", ((JObject)((JArray)((JObject)final.Req[0])["itm"]!)[0])["p"]!.ToString()); // renumbered after reorder
|
||||||
|
Assert.Equal(130m, final.Sums.TotalNet); // totals stable across the whole sequence
|
||||||
|
Assert.Equal(24.7m, final.Sums.TotalVat); // VAT restored after the toggle roundtrip
|
||||||
|
Assert.Equal(4, final.History.Count);
|
||||||
|
Assert.DoesNotContain(final.ValidationMessages, m => m.Severity == "error");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,12 +38,18 @@ public class InvoiceOptionsTests
|
|||||||
=> Assert.Equal("setmode:setprice", InvoiceOptionsFor(new { type = "r", setmode = "setprice" }));
|
=> Assert.Equal("setmode:setprice", InvoiceOptionsFor(new { type = "r", setmode = "setprice" }));
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("itemprices", "setmode:itemprices")]
|
|
||||||
[InlineData("setonly", "setmode:setonly")]
|
[InlineData("setonly", "setmode:setonly")]
|
||||||
[InlineData("ITEMPRICES", "setmode:itemprices")] // case-insensitive
|
[InlineData("SETONLY", "setmode:setonly")] // case-insensitive
|
||||||
public void SetMode_EmitsToken(string mode, string expected)
|
public void SetMode_EmitsToken(string mode, string expected)
|
||||||
=> Assert.Equal(expected, InvoiceOptionsFor(new { type = "r", setmode = mode }));
|
=> Assert.Equal(expected, InvoiceOptionsFor(new { type = "r", setmode = mode }));
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemovedItemPricesMode_OmitsToken()
|
||||||
|
// "itemprices" was a valid mode before the button/mode was removed. Any invoice options
|
||||||
|
// still carrying the stale value (or a client re-posting it) must not be persisted as a
|
||||||
|
// recognized token — it is treated the same as an unknown/garbage mode.
|
||||||
|
=> Assert.Equal("", InvoiceOptionsFor(new { type = "r", setmode = "itemprices" }));
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void UnknownSetMode_OmitsToken()
|
public void UnknownSetMode_OmitsToken()
|
||||||
=> Assert.Equal("", InvoiceOptionsFor(new { type = "r", setmode = "garbage" }));
|
=> Assert.Equal("", InvoiceOptionsFor(new { type = "r", setmode = "garbage" }));
|
||||||
@@ -106,9 +112,11 @@ public class InvoiceOptionsTests
|
|||||||
public void ModeFromInvoiceOptions_RoundTripsBackendEmission()
|
public void ModeFromInvoiceOptions_RoundTripsBackendEmission()
|
||||||
{
|
{
|
||||||
// The token this side emits must parse back to the same mode on the PDF side.
|
// The token this side emits must parse back to the same mode on the PDF side.
|
||||||
Assert.Equal(SetDisplayMode.ItemPrices,
|
|
||||||
InvoiceSetPricing.ModeFromInvoiceOptions(InvoiceOptionsFor(new { type = "r", setmode = "itemprices" })));
|
|
||||||
Assert.Equal(SetDisplayMode.SetOnly,
|
Assert.Equal(SetDisplayMode.SetOnly,
|
||||||
InvoiceSetPricing.ModeFromInvoiceOptions(InvoiceOptionsFor(new { type = "r", p13b = true, setmode = "setonly" })));
|
InvoiceSetPricing.ModeFromInvoiceOptions(InvoiceOptionsFor(new { type = "r", p13b = true, setmode = "setonly" })));
|
||||||
|
// A removed mode never round-trips as itself — it is never persisted, so reading it back
|
||||||
|
// always yields the default SetPrice.
|
||||||
|
Assert.Equal(SetDisplayMode.SetPrice,
|
||||||
|
InvoiceSetPricing.ModeFromInvoiceOptions(InvoiceOptionsFor(new { type = "r", setmode = "itemprices" })));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Fuchs.intranet;
|
using Fuchs.intranet;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@@ -40,11 +40,11 @@ public class InvoiceSetPricingTests
|
|||||||
|
|
||||||
// ── Mode parsing ────────────────────────────────────────────────────────
|
// ── Mode parsing ────────────────────────────────────────────────────────
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("itemprices", SetDisplayMode.ItemPrices)]
|
|
||||||
[InlineData("items", SetDisplayMode.ItemPrices)]
|
|
||||||
[InlineData("setonly", SetDisplayMode.SetOnly)]
|
[InlineData("setonly", SetDisplayMode.SetOnly)]
|
||||||
|
[InlineData("set_only", SetDisplayMode.SetOnly)]
|
||||||
[InlineData("setprice", SetDisplayMode.SetPrice)]
|
[InlineData("setprice", SetDisplayMode.SetPrice)]
|
||||||
[InlineData("", SetDisplayMode.SetPrice)]
|
[InlineData("", SetDisplayMode.SetPrice)]
|
||||||
|
[InlineData("itemprices", SetDisplayMode.SetPrice)] // removed mode — falls back to default, never throws
|
||||||
[InlineData("garbage", SetDisplayMode.SetPrice)]
|
[InlineData("garbage", SetDisplayMode.SetPrice)]
|
||||||
public void ParseMode_Works(string raw, SetDisplayMode expected)
|
public void ParseMode_Works(string raw, SetDisplayMode expected)
|
||||||
=> Assert.Equal(expected, InvoiceSetPricing.ParseMode(raw));
|
=> Assert.Equal(expected, InvoiceSetPricing.ParseMode(raw));
|
||||||
@@ -52,10 +52,12 @@ public class InvoiceSetPricingTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void ModeFromInvoiceOptions_ReadsToken()
|
public void ModeFromInvoiceOptions_ReadsToken()
|
||||||
{
|
{
|
||||||
Assert.Equal(SetDisplayMode.ItemPrices, InvoiceSetPricing.ModeFromInvoiceOptions("§13b,setmode:itemprices"));
|
Assert.Equal(SetDisplayMode.SetOnly, InvoiceSetPricing.ModeFromInvoiceOptions("§13b,setmode:setonly"));
|
||||||
Assert.Equal(SetDisplayMode.SetOnly, InvoiceSetPricing.ModeFromInvoiceOptions("setmode:setonly"));
|
|
||||||
Assert.Equal(SetDisplayMode.SetPrice, InvoiceSetPricing.ModeFromInvoiceOptions("§13b")); // default
|
Assert.Equal(SetDisplayMode.SetPrice, InvoiceSetPricing.ModeFromInvoiceOptions("§13b")); // default
|
||||||
Assert.Equal(SetDisplayMode.SetPrice, InvoiceSetPricing.ModeFromInvoiceOptions(null));
|
Assert.Equal(SetDisplayMode.SetPrice, InvoiceSetPricing.ModeFromInvoiceOptions(null));
|
||||||
|
// A stale/removed "itemprices" token (e.g. from an invoice created before the mode was
|
||||||
|
// dropped) must not throw — it degrades gracefully to the default SetPrice mode.
|
||||||
|
Assert.Equal(SetDisplayMode.SetPrice, InvoiceSetPricing.ModeFromInvoiceOptions("setmode:itemprices"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -85,22 +87,6 @@ public class InvoiceSetPricingTests
|
|||||||
Assert.Equal(50.00m, lines[3].TotalNet);
|
Assert.Equal(50.00m, lines[3].TotalNet);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── ItemPrices: members priced, set header blank ──────────────────────────
|
|
||||||
[Fact]
|
|
||||||
public void Build_ItemPrices_MembersPricedHeaderBlank()
|
|
||||||
{
|
|
||||||
var lines = InvoiceSetPricing.Build(Sample(), SetDisplayMode.ItemPrices);
|
|
||||||
|
|
||||||
Assert.Equal(4, lines.Count);
|
|
||||||
Assert.True(lines[0].IsSetHeader);
|
|
||||||
Assert.False(lines[0].ShowPrice); // set header is just a title now
|
|
||||||
Assert.True(lines[1].ShowPrice);
|
|
||||||
Assert.Equal(600.00m, lines[1].TotalNet);
|
|
||||||
Assert.True(lines[2].ShowPrice);
|
|
||||||
Assert.Equal(400.00m, lines[2].TotalNet);
|
|
||||||
Assert.True(lines[3].ShowPrice); // standalone
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── SetOnly: members removed ──────────────────────────────────────────────
|
// ── SetOnly: members removed ──────────────────────────────────────────────
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Build_SetOnly_RemovesMembers()
|
public void Build_SetOnly_RemovesMembers()
|
||||||
@@ -115,18 +101,45 @@ public class InvoiceSetPricingTests
|
|||||||
Assert.Equal("Anfahrt", lines[1].Title);
|
Assert.Equal("Anfahrt", lines[1].Title);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Set price falls back to sum of members when header total is 0 ─────────
|
// ── Unconverted set (header total still 0): shown blank, members individually priced ──
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Build_HeaderTotalZero_UsesSumOfMembers()
|
public void Build_HeaderTotalZero_UnconvertedSet_HeaderBlankMembersPriced()
|
||||||
{
|
{
|
||||||
var items = new List<Dictionary<string, object?>>
|
var items = new List<Dictionary<string, object?>>
|
||||||
{
|
{
|
||||||
SetHeader("7", "Set ohne Preis"), // total 0
|
SetHeader("7", "Set ohne Preis"), // total 0 — not yet converted
|
||||||
Member("7", "A", "120.00", "120.00"),
|
Member("7", "A", "120.00", "120.00"),
|
||||||
Member("7", "B", "80.00", "80.00")
|
Member("7", "B", "80.00", "80.00")
|
||||||
};
|
};
|
||||||
var lines = InvoiceSetPricing.Build(items, SetDisplayMode.SetPrice);
|
var lines = InvoiceSetPricing.Build(items, SetDisplayMode.SetPrice);
|
||||||
Assert.Equal(200.00m, lines[0].TotalNet); // 120 + 80
|
|
||||||
|
Assert.Equal(3, lines.Count);
|
||||||
|
Assert.True(lines[0].IsSetHeader);
|
||||||
|
Assert.False(lines[0].ShowPrice); // header not priced yet
|
||||||
|
Assert.Equal(0m, lines[0].TotalNet);
|
||||||
|
|
||||||
|
Assert.True(lines[1].ShowPrice); // members keep their own price
|
||||||
|
Assert.Equal(120.00m, lines[1].TotalNet);
|
||||||
|
Assert.True(lines[2].ShowPrice);
|
||||||
|
Assert.Equal(80.00m, lines[2].TotalNet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SetOnly on an unconverted set also just passes items through unchanged ──
|
||||||
|
[Fact]
|
||||||
|
public void Build_HeaderTotalZero_UnconvertedSet_SetOnlyModeStillPassesThrough()
|
||||||
|
{
|
||||||
|
var items = new List<Dictionary<string, object?>>
|
||||||
|
{
|
||||||
|
SetHeader("7", "Set ohne Preis"),
|
||||||
|
Member("7", "A", "120.00", "120.00"),
|
||||||
|
Member("7", "B", "80.00", "80.00")
|
||||||
|
};
|
||||||
|
var lines = InvoiceSetPricing.Build(items, SetDisplayMode.SetOnly);
|
||||||
|
|
||||||
|
Assert.Equal(3, lines.Count); // members not dropped before conversion
|
||||||
|
Assert.False(lines[0].ShowPrice);
|
||||||
|
Assert.True(lines[1].ShowPrice);
|
||||||
|
Assert.True(lines[2].ShowPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── No sets: pass-through unchanged ───────────────────────────────────────
|
// ── No sets: pass-through unchanged ───────────────────────────────────────
|
||||||
@@ -164,7 +177,7 @@ public class InvoiceSetPricingTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Build_TextLine_AsSetMember_NoPriceEvenInItemPrices()
|
public void Build_TextLine_AsSetMember_NoPriceInSetPriceMode()
|
||||||
{
|
{
|
||||||
var items = new List<Dictionary<string, object?>>
|
var items = new List<Dictionary<string, object?>>
|
||||||
{
|
{
|
||||||
@@ -172,10 +185,10 @@ public class InvoiceSetPricingTests
|
|||||||
new() { ["type"] = "title", ["setId"] = "10", ["title"] = "Hinweis", ["total_net"] = "" },
|
new() { ["type"] = "title", ["setId"] = "10", ["title"] = "Hinweis", ["total_net"] = "" },
|
||||||
Member("10", "Waschbecken", "600.00", "600.00")
|
Member("10", "Waschbecken", "600.00", "600.00")
|
||||||
};
|
};
|
||||||
var lines = InvoiceSetPricing.Build(items, SetDisplayMode.ItemPrices);
|
var lines = InvoiceSetPricing.Build(items, SetDisplayMode.SetPrice);
|
||||||
var note = lines.First(l => l.Title == "Hinweis");
|
var note = lines.First(l => l.Title == "Hinweis");
|
||||||
Assert.False(note.ShowPrice); // text member stays blank
|
Assert.False(note.ShowPrice); // text member stays blank (already blank in SetPrice mode)
|
||||||
Assert.True(lines.First(l => l.Title == "Waschbecken").ShowPrice);
|
Assert.False(lines.First(l => l.Title == "Waschbecken").ShowPrice); // members blank in SetPrice mode too
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Set price equals sum of member prices across modes (no double counting) ─
|
// ── Set price equals sum of member prices across modes (no double counting) ─
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ The **Fuchs Intranet** solution is a line-of-business web application for **Seba
|
|||||||
| Project | Type | Purpose |
|
| Project | Type | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **Fuchs** | ASP.NET Core Web (MVC) | Main web application — the intranet |
|
| **Fuchs** | ASP.NET Core Web (MVC) | Main web application — the intranet |
|
||||||
| **Fuchs_DataService** | Console / Windows Service (Topshelf) | Background data sync service (MFR ERP polling) |
|
| **Fuchs_DataService** | Class Library | MFR ERP sync logic (entity polling, invoice/DATEV files). Hosted in-process by **Fuchs** as a `PeriodicHostedService` (see ADR 0010) — no longer a standalone process. |
|
||||||
| **MFR_RESTClient** | Class Library | REST/OData client for the MFR ERP system. The REST/OData contract is documented in `MFR_RESTClient/Docs/mfr_interface_description.md`. |
|
| **MFR_RESTClient** | Class Library | REST/OData client for the MFR ERP system. The REST/OData contract is documented in `MFR_RESTClient/Docs/mfr_interface_description.md`. |
|
||||||
| **Fuchs_Database** | SSDT (SQL project) | Source of truth for the `fuchs_fds` SQL schema (tables, table types, functions, stored procedures the backend calls). |
|
| **Fuchs_Database** | SSDT (SQL project) | Source of truth for the `fuchs_fds` SQL schema (tables, table types, functions, stored procedures the backend calls). |
|
||||||
| **OCORE** | Class Library (shared) | Core utilities: SQL, crypto, email, IO, logging |
|
| **OCORE** | Class Library (shared) | Core utilities: SQL, crypto, email, IO, logging |
|
||||||
@@ -96,17 +96,18 @@ The **Fuchs Intranet** solution is a line-of-business web application for **Seba
|
|||||||
│ │
|
│ │
|
||||||
▼ ▼
|
▼ ▼
|
||||||
┌─────────────────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────────────────┐
|
||||||
│ Fuchs_DataService (Windows Service / Console) │
|
│ Fuchs_DataService (Class Library, hosted in-process) │
|
||||||
│ │
|
│ │
|
||||||
│ FdsMain.cs — Topshelf host, job definitions │
|
│ PeriodicHostedService — in Fuchs/Services; BackgroundService + │
|
||||||
│ PeriodicHostedService — BackgroundService with PeriodicTimer │
|
│ PeriodicTimer, registered in Program.cs │
|
||||||
|
│ when Fds:SyncEnabled (ADR 0010) │
|
||||||
│ FdsMfr.cs (IFdsMfr) — MFR sync orchestration │
|
│ FdsMfr.cs (IFdsMfr) — MFR sync orchestration │
|
||||||
│ FdsMfrClient.cs — MFR REST client wrapper │
|
│ FdsMfrClient.cs — MFR REST client wrapper │
|
||||||
│ FdsShared.cs — FdsConfig (appsettings.json reader) │
|
│ FdsShared.cs — FdsConfig (reads the host's IConfiguration) │
|
||||||
│ FdsZip.cs — 7-Zip archive handling (DATEV export) │
|
│ (DATEV zip) — OCORE.zip (System.IO.Compression), no 7-Zip │
|
||||||
│ FdsDebug.cs — Debug/file logging │
|
│ FdsDebug.cs — Debug/file logging │
|
||||||
│ │
|
│ │
|
||||||
│ Jobs: MfrSync (every N min) │
|
│ Jobs: MfrSync (every Fds:ExecutionFrequency_Minutes) │
|
||||||
│ → UpdateIfNecessary_async (entity table sync) │
|
│ → UpdateIfNecessary_async (entity table sync) │
|
||||||
│ → UpdateRequested_async (on-demand entity refresh) │
|
│ → UpdateRequested_async (on-demand entity refresh) │
|
||||||
│ → GetInvoiceFiles_async (invoice PDF download) │
|
│ → GetInvoiceFiles_async (invoice PDF download) │
|
||||||
@@ -176,7 +177,7 @@ Stateless services (`IPdfService`, `IBankingService`, `IMfrClientFactory`) are s
|
|||||||
There is no ORM (no EF Core). All data access uses **ADO.NET via OCORE SQL helpers** (`getSQLDatatable_async`, `getSQLDataSet_async`, `setSQLValue_async`) calling stored procedures and inline SQL. `DataTable`/`DataRow` is the primary data transfer mechanism.
|
There is no ORM (no EF Core). All data access uses **ADO.NET via OCORE SQL helpers** (`getSQLDatatable_async`, `getSQLDataSet_async`, `setSQLValue_async`) calling stored procedures and inline SQL. `DataTable`/`DataRow` is the primary data transfer mechanism.
|
||||||
|
|
||||||
### 4.5 Background Service
|
### 4.5 Background Service
|
||||||
`Fuchs_DataService` runs as a Windows Service (Topshelf) with a `PeriodicHostedService` that polls the MFR ERP system on a timer, syncing entities and downloading invoice files.
|
The MFR ERP sync runs **in-process inside the web app** as a `PeriodicHostedService` (`Fuchs/Services/PeriodicHostedService.cs`), registered in `Program.cs` when `Fds:SyncEnabled` is true. It polls the MFR ERP on a timer (`Fds:ExecutionFrequency_Minutes`), syncing entities and downloading invoice files via the sync logic in the `Fuchs_DataService` library. See ADR [0010](Decisions/0010-mfr-sync-hosted-in-web-app.md).
|
||||||
|
|
||||||
### 4.6 Authentication
|
### 4.6 Authentication
|
||||||
Cookie-based authentication (`CookieAuthenticationDefaults`) with custom claims (`FuchsUserIdentity`). SQL-based user/password verification.
|
Cookie-based authentication (`CookieAuthenticationDefaults`) with custom claims (`FuchsUserIdentity`). SQL-based user/password verification.
|
||||||
@@ -409,7 +410,7 @@ public class MfrClientFactory : IMfrClientFactory, IDisposable
|
|||||||
2. ✅ **Resolved** — `FdsInvoiceData`/`FdsReminderData` are now pure data holders; DB + PDF logic moved to `IInvoiceService`/`IReminderService`.
|
2. ✅ **Resolved** — `FdsInvoiceData`/`FdsReminderData` are now pure data holders; DB + PDF logic moved to `IInvoiceService`/`IReminderService`.
|
||||||
3. ✅ **Resolved** — `FdsMfrClient` is created via `IMfrClientFactory` (no `new` in controllers).
|
3. ✅ **Resolved** — `FdsMfrClient` is created via `IMfrClientFactory` (no `new` in controllers).
|
||||||
4. ✅ **Resolved** — `OCORE_Charting` is now used (transitively, via `OCORE_web`'s chart engine) by the report renderer (`FuchsVisualization`).
|
4. ✅ **Resolved** — `OCORE_Charting` is now used (transitively, via `OCORE_web`'s chart engine) by the report renderer (`FuchsVisualization`).
|
||||||
5. ⏳ **Open** — **Topshelf** in `Fuchs_DataService` could be replaced with native `dotnet` Worker Service hosting for .NET 10 alignment.
|
5. ✅ **Resolved** — **Topshelf** removed; `Fuchs_DataService` is now a class library and the MFR sync is hosted in-process by the web app as a `PeriodicHostedService` (ADR 0010).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
---
|
---
|
||||||
status: Active
|
status: Active
|
||||||
lastUpdated: 2026-07-10
|
lastUpdated: 2026-07-10
|
||||||
applyTo:
|
applyTo:
|
||||||
@@ -55,13 +55,19 @@ Expiry: Server (timer) --SignalR draftExpiring{token,secondsLeft}--> warn "bit
|
|||||||
exact editor JSON (`admin` / `new` / `req` blocks with `items`), plus server-computed
|
exact editor JSON (`admin` / `new` / `req` blocks with `items`), plus server-computed
|
||||||
`Sums`, `ValidationMessages`, `History`, `Version`, `Token`, `InvId`, `LastAccessUtc`.
|
`Sums`, `ValidationMessages`, `History`, `Version`, `Token`, `InvId`, `LastAccessUtc`.
|
||||||
- **Calculation** (`InvoiceDraftCalculator`, static/pure) ports the former client math:
|
- **Calculation** (`InvoiceDraftCalculator`, static/pure) ports the former client math:
|
||||||
`RecomputeItem` (quantity × price × VAT, the `quantChange` port), `RecomputeTotals`
|
`RecomputeLineValues` (net/VAT/service-net/service-VAT per line from raw quantity ×
|
||||||
|
price × VAT rate, the `quantChange`/`setVat` port), `RecomputeTotals`
|
||||||
(the `invSumUpdate`/`csms` aggregation + §13b reverse-charge), `RecomputePositions`
|
(the `invSumUpdate`/`csms` aggregation + §13b reverse-charge), `RecomputePositions`
|
||||||
(numbers every line except heading/free-text lines continuously across the whole invoice —
|
(numbers every line except heading/free-text lines continuously across the whole invoice —
|
||||||
mirroring the editor's `invSumUpdate`, so the editor and the PDF show identical `Pos.` numbers,
|
mirroring the editor's `invSumUpdate`, so the editor and the PDF show identical `Pos.` numbers,
|
||||||
including after a reorder),
|
including after a reorder),
|
||||||
and `Validate` (email/address/items/VAT-rate/negative-total checks). Being pure, it is
|
and `Validate` (email/address/items/VAT-rate/negative-total checks). Being pure, it is
|
||||||
exhaustively unit-tested.
|
exhaustively unit-tested. **The online editor performs no arithmetic of any kind** — not
|
||||||
|
header, footer, sums, totals, taxes, nor a single line's own net/VAT value: `$inv.quantChange`
|
||||||
|
and `$inv.setVat` only post the raw field the user changed (qty/price/vat rate), and
|
||||||
|
`$inv.invSumUpdate` only reassembles the row-contract array needed to post `req` — it computes
|
||||||
|
no totals, no VAT breakdown, and no service-refund note figures. All of those are rendered
|
||||||
|
exclusively from `dstate.sums` via `$inv.d.footer`.
|
||||||
- **Sanitisation & reorder.** Scalar text deltas (`title`/`email`/`address`/`provisionperiod`/
|
- **Sanitisation & reorder.** Scalar text deltas (`title`/`email`/`address`/`provisionperiod`/
|
||||||
`provisionlocation`) and the section heading (`block.replace`) are stripped of the editor's
|
`provisionlocation`) and the section heading (`block.replace`) are stripped of the editor's
|
||||||
TinyMCE HTML (`<p>…</p>`, `<br>`) to plain text in `ApplyDelta` (`HtmlToPlain`) — the backend
|
TinyMCE HTML (`<p>…</p>`, `<br>`) to plain text in `ApplyDelta` (`HtmlToPlain`) — the backend
|
||||||
@@ -85,7 +91,8 @@ Expiry: Server (timer) --SignalR draftExpiring{token,secondsLeft}--> warn "bit
|
|||||||
`draftClosed`. Business success/failure still flows through `IEventService`/`NotificationHub`.
|
`draftClosed`. Business success/failure still flows through `IEventService`/`NotificationHub`.
|
||||||
- **Frontend** (`$fis.draft` in `fis_main.js`, editor in `fis.inv_shared.js`) opens/joins,
|
- **Frontend** (`$fis.draft` in `fis_main.js`, editor in `fis.inv_shared.js`) opens/joins,
|
||||||
posts one delta per change, shows a loading state whenever awaiting a signal, and offers
|
posts one delta per change, shows a loading state whenever awaiting a signal, and offers
|
||||||
"Änderungen verwerfen" and "Änderungshistorie" menu actions. It no longer computes totals.
|
"Änderungen verwerfen" and "Änderungshistorie" menu actions. It computes nothing — no
|
||||||
|
header, footer, sums, totals, taxes, or per-line values.
|
||||||
|
|
||||||
## Key files
|
## Key files
|
||||||
- `Fuchs/code/InvoiceDraftSession.cs` — session + `ChangeHistoryEntry` + `InvoiceDraftSums`.
|
- `Fuchs/code/InvoiceDraftSession.cs` — session + `ChangeHistoryEntry` + `InvoiceDraftSums`.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
---
|
---
|
||||||
status: Accepted
|
status: Accepted
|
||||||
date: 2026-07-10
|
date: 2026-07-10
|
||||||
applyTo:
|
applyTo:
|
||||||
@@ -36,9 +36,11 @@ singleton `IInvoiceDraftCache` and orchestrated by the scoped `IInvoiceDraftServ
|
|||||||
(`InvoiceDraftEditService`). The browser is a pure view/input layer.
|
(`InvoiceDraftEditService`). The browser is a pure view/input layer.
|
||||||
|
|
||||||
- **Truth & calculation on the server.** `InvoiceDraftCalculator` is the pure,
|
- **Truth & calculation on the server.** `InvoiceDraftCalculator` is the pure,
|
||||||
unit-tested port of the former client-side math (`quantChange` + `invSumUpdate`),
|
unit-tested port of the former client-side math (`quantChange` + `setVat` +
|
||||||
including the §13b reverse-charge rule and VAT-per-rate grouping. The browser never
|
`invSumUpdate`), including per-line net/VAT/service-value multiplication
|
||||||
computes totals; it renders the server's `sums`.
|
(`RecomputeLineValues`), the §13b reverse-charge rule and VAT-per-rate grouping. The
|
||||||
|
browser performs **no arithmetic whatsoever** — not even a single line's
|
||||||
|
`net = qty × price` — it only renders the server's `req`/`sums`.
|
||||||
- **Commands are ordinary POSTs; signals are SignalR.** The editor posts single edits
|
- **Commands are ordinary POSTs; signals are SignalR.** The editor posts single edits
|
||||||
to `inv/dpatch` (and `dopen`/`dstate`/`dpreview`/`dsave`/`dhistory`/`ddiscard`/`dclose`).
|
to `inv/dpatch` (and `dopen`/`dstate`/`dpreview`/`dsave`/`dhistory`/`ddiscard`/`dclose`).
|
||||||
The server mutates the session, recomputes, validates, bumps a version, and pings the
|
The server mutates the session, recomputes, validates, bumps a version, and pings the
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
---
|
||||||
|
status: Accepted
|
||||||
|
date: 2026-07-15
|
||||||
|
applyTo:
|
||||||
|
- "Fuchs/Services/InvoiceDraft*"
|
||||||
|
- "Fuchs/Services/IInvoiceDraft*"
|
||||||
|
- "Fuchs/Services/ReminderDraft*"
|
||||||
|
- "Fuchs/Services/IReminderDraft*"
|
||||||
|
- "Fuchs/code/InvoiceDraftSession.cs"
|
||||||
|
- "Fuchs/code/InvoiceDraftCalculator.cs"
|
||||||
|
- "Fuchs/code/InvoiceSetPricing.cs"
|
||||||
|
- "Fuchs/code/ReminderDraftSession.cs"
|
||||||
|
- "Fuchs/code/ReminderDraftCalculator.cs"
|
||||||
|
- "Fuchs/code/FuchsPdf.cs"
|
||||||
|
- "Fuchs/js/intranet/**"
|
||||||
|
supersededBy: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
# 0008 — Invoices and reminders (all kinds) are fully backend-authoritative; PDF and online editor must render identical content
|
||||||
|
|
||||||
|
## Context
|
||||||
|
ADR [0006](0006-backend-authoritative-draft-editing.md) established the backend-authoritative
|
||||||
|
draft-editing model for invoices and noted reminders were "intended to follow the identical
|
||||||
|
pattern as a second phase". Both are now implemented (`InvoiceDraftEditService` /
|
||||||
|
`ReminderDraftEditService`). In practice, ambiguity kept resurfacing about *which* invoice/
|
||||||
|
reminder kinds this covers and *which* kinds of change qualify as "must be computed server-side":
|
||||||
|
e.g. whether a purely presentational client-side re-render (set-price display toggle, item
|
||||||
|
reordering, position renumbering) was allowed to keep any client-side math, and whether this
|
||||||
|
applies uniformly to every invoice type (regular `r`, partial/Abschlag `i`, final `f`, storno
|
||||||
|
`c`) and every reminder stage, not just the pilot "regular invoice" flow. This decision closes
|
||||||
|
that ambiguity explicitly.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
**Every invoice (all `InvoiceType` kinds: regular, partial/Abschlagsrechnung, final/
|
||||||
|
Schlussrechnung, Storno/credit) and every reminder (all reminder stages/Mahnstufen) is
|
||||||
|
backend-authoritative while being drafted or previewed.** This generalises and makes explicit
|
||||||
|
what ADR 0006 already implied for the pilot flow:
|
||||||
|
|
||||||
|
- **Any calculation** (net/VAT/gross totals, per-rate VAT grouping, service-refund figures,
|
||||||
|
§13b reverse-charge suppression, set-price sums, open-amount for reminders, position/line
|
||||||
|
numbering) is performed exclusively by the server (`InvoiceDraftCalculator`,
|
||||||
|
`ReminderDraftCalculator`, `InvoiceSetPricing`). The browser never sums, subtracts, or
|
||||||
|
otherwise derives a monetary or positional value — it only displays server-computed values.
|
||||||
|
This includes the single-line arithmetic that used to run in `quantChange`/`setVat`
|
||||||
|
(`net_val = qty × price`, `vat_val = net_val × rate`, service-net/-VAT splits): those
|
||||||
|
handlers now only post the raw, unmultiplied field the user typed (`qn`/`v`/`vat`) and the
|
||||||
|
server (`InvoiceDraftCalculator.RecomputeLineValues`) computes every derived line value.
|
||||||
|
Likewise the invoice footer (net/VAT-by-rate/gross), the per-block "isum" cell, and the
|
||||||
|
service-refund note figures are rendered exclusively from `dstate.sums` (`$inv.d.footer`);
|
||||||
|
`$inv.invSumUpdate` no longer accumulates any of these — it only reassembles the row
|
||||||
|
contract array needed to post `req` to the server and (on first load) seeds the session.
|
||||||
|
- **Any setting** (§13b flag, set-pricing display mode, payment terms, contact, custom values,
|
||||||
|
…) is applied server-side via a named `InvoiceDraftDelta`/`ReminderDraftDelta` target and
|
||||||
|
reflected back through `dstate`. The client never mutates its local model as the source of
|
||||||
|
truth for a setting; it optimistically reflects the *request* but always re-renders from the
|
||||||
|
next `dstate`/`draftReady` refresh.
|
||||||
|
- **Any text change** (recipient email/address, invoice title, provision location/period,
|
||||||
|
section headings, item name/description/notes) is sanitised and stored server-side
|
||||||
|
(`InvoiceDraftEditService.HtmlToPlain` et al.); the server's stored value is the one that
|
||||||
|
reaches the PDF and any reloaded draft.
|
||||||
|
- **Any reordering** (drag-reorder of service-request blocks/sections, drag-reorder of item
|
||||||
|
rows within a block) is committed as a `block.order` (or equivalent) delta; the server
|
||||||
|
performs the actual reorder and renumbers positions (`InvoiceDraftCalculator.RecomputePositions`).
|
||||||
|
The client's drag interaction is input only — the rendered order after a refresh is the
|
||||||
|
server's order, not whatever the browser left in the DOM mid-drag.
|
||||||
|
- **Irreversible one-way conversions** (e.g. "Auf Setpreis umstellen" — switching a set's
|
||||||
|
member items from individual prices to a single set price) are likewise backend-only
|
||||||
|
operations (`item.setprice` delta / `InvoiceDraftEditService.ApplyItemSetPrice`), never
|
||||||
|
computed or applied in the browser.
|
||||||
|
- **The PDF must render 100% the same information and content as the online editor at any
|
||||||
|
given moment.** Both consume the identical authoritative session data:
|
||||||
|
- The online editor renders `dstate`'s `req`/`sums`/`setDisplay`/`notes` — all server-computed.
|
||||||
|
- The PDF preview (`inv/dpreview`, `rem/dpreview`) renders straight from the same cached
|
||||||
|
session via a synthesised registration (`InvoiceDraftEditService.RenderPreview` /
|
||||||
|
`ReminderDraftEditService`'s reminder equivalent) — **not** from a separate client upload
|
||||||
|
or a re-derived model.
|
||||||
|
- `FuchsPdf.BuildInvoiceNotes` (notice paragraphs) is called identically for both the
|
||||||
|
editor's `notes` array and the PDF body, so intro/closing texts can never drift between
|
||||||
|
the two renderings.
|
||||||
|
- Any new editor-visible fact (a new total, a new flag, a new note) must be added to the
|
||||||
|
shared session/service layer once, not duplicated as separate editor-only and PDF-only
|
||||||
|
logic.
|
||||||
|
- This applies for the full lifecycle while a document is a draft (open → edit → preview →
|
||||||
|
Zwischenspeichern) up to finalise; a finalised, persisted invoice/reminder is immutable
|
||||||
|
and is rendered straight from its stored DB data (no draft session involved) — that path
|
||||||
|
already has no client-side math to begin with.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- New invoice/reminder editor features must be modelled as a server-side delta + calculator
|
||||||
|
change, exactly as ADR 0006 already requires; this decision removes any residual excuse to
|
||||||
|
special-case a "just this one is presentational, do it in JS" shortcut for reordering,
|
||||||
|
display-mode toggles, or one-way conversions.
|
||||||
|
- Any PDF-only or editor-only special-casing found in review is a bug against this decision —
|
||||||
|
the shared session/service must be extended so both renderers read the same value/flag.
|
||||||
|
- Reminder "Mahnstufen" and every invoice type share this obligation; there is no partial/
|
||||||
|
Abschlagsrechnung, Schlussrechnung, or Storno exemption while such a document is still a
|
||||||
|
draft going through the same `dopen`/`dpatch`/`dpreview`/`dsave` flow.
|
||||||
|
- Test coverage for the cache/session layer (`InvoiceDraftEditService`, `ReminderDraftEditService`,
|
||||||
|
`InvoiceDraftCalculator`, `InvoiceSetPricing`) must exercise every mutating operation
|
||||||
|
(text edits, reordering, all three set-pricing display modes, the set-price conversion,
|
||||||
|
multi-rate VAT sums, full recompute) against mock datasets, since this is now the single
|
||||||
|
place all of these behaviours are guaranteed correct — see `Fuchs.Tests/InvoiceDraftServiceTests.cs`,
|
||||||
|
`Fuchs.Tests/ReminderDraftServiceTests.cs`, `Fuchs.Tests/InvoiceDraftCalculatorTests.cs`,
|
||||||
|
`Fuchs.Tests/InvoiceSetPricingTests.cs`.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
- **Scope this only to the invoice pilot flow** (leave reminders/other invoice kinds
|
||||||
|
ambiguous): rejected — the ambiguity itself was the problem being fixed; the underlying
|
||||||
|
session/service code already treats all kinds uniformly, so documenting anything narrower
|
||||||
|
would misrepresent the code.
|
||||||
|
- **Allow "purely cosmetic" client-side math for reordering/display toggles**: rejected —
|
||||||
|
history showed exactly this exception is where drift crept in (e.g. the set-price toggle
|
||||||
|
originally computed sums in the browser before being moved server-side); no exception is
|
||||||
|
granted.
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
---
|
||||||
|
status: Accepted
|
||||||
|
date: 2026-07-14
|
||||||
|
applyTo:
|
||||||
|
- "Fuchs/code/InvoiceSetPricing.cs"
|
||||||
|
- "Fuchs/Services/InvoiceDraft*"
|
||||||
|
- "Fuchs/Services/IInvoiceDraft*"
|
||||||
|
- "Fuchs/code/InvoiceDraftSession.cs"
|
||||||
|
- "Fuchs/code/InvoiceDraftCalculator.cs"
|
||||||
|
- "Fuchs/code/FuchsPdf.cs"
|
||||||
|
- "Fuchs/js/intranet/**"
|
||||||
|
- "Fuchs/Docs/INVOICE_SET_PRICING.md"
|
||||||
|
supersededBy: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
# 0009 — The two menu set-price modes are per-service-request-block, irreversible cache mutations that insert a dedicated set row
|
||||||
|
|
||||||
|
## Context
|
||||||
|
ADRs [0006](0006-backend-authoritative-draft-editing.md) and
|
||||||
|
[0008](0008-invoices-and-reminders-fully-backend-authoritative.md) made draft editing
|
||||||
|
backend-authoritative. Under that model the set-price feature had **three** functions, of
|
||||||
|
which the two menu-driven ones ("Set mit Preis" / "Nur Set mit Preis") were framed as
|
||||||
|
whole-invoice **display modes** (`SetDisplayMode.SetPrice`/`SetOnly`): a non-mutating,
|
||||||
|
render-time transform (`InvoiceSetPricing.Build`) over explicit `type == "set"` header items
|
||||||
|
and their `SetItmId` members, persisted only as an `admin.setmode` flag. ADR 0008 calls them
|
||||||
|
"display-mode toggles" and treats them as presentational.
|
||||||
|
|
||||||
|
The product owner has redefined those two menu functions. They are **not** display toggles and
|
||||||
|
they are **not** keyed on set-item membership:
|
||||||
|
|
||||||
|
- Their grouping is the **service request** (`ServiceRequestId` = the editor's tbody block),
|
||||||
|
never `SetItmId`. Every block is treated as one set, whether or not it contains any
|
||||||
|
`type == "set"` item.
|
||||||
|
- Applying a mode is an **irreversible data change** written hard into the cached draft
|
||||||
|
dataset — not a reversible view flag. There is no toggle back; the user adjusts the result
|
||||||
|
by hand afterwards.
|
||||||
|
|
||||||
|
This decision records that redefinition. It **refines** ADR 0008's characterisation of these
|
||||||
|
two operations (from "non-mutating display toggle" to "mutating, one-way conversion"); ADR
|
||||||
|
0008's broader rule — every calculation server-side, and the PDF renders 100% the same content
|
||||||
|
as the editor — remains fully in force and is not superseded.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
There are three distinct set-price operations, kept clearly separated:
|
||||||
|
|
||||||
|
1. **Inline set-item switch — unchanged.** The row context button on a single `type == "set"`
|
||||||
|
item (`$inv.toSetPrice` → `item.setprice` delta → `InvoiceDraftEditService.ApplyItemSetPrice`).
|
||||||
|
It is `SetItmId`-based, sums the header's members onto the header, sets the members' prices to
|
||||||
|
`null` (empty cell, excluded from the sum — consistent with modes 2 & 3, not `0`), and is
|
||||||
|
one-way. This is the **only** set-price operation that reads `SetItmId`. The button is shown —
|
||||||
|
and the operation available — **only** for items that are `type == "set"` **and** carry a
|
||||||
|
`SetItmId` (and are still unconverted, i.e. own price `0`); an item missing either condition
|
||||||
|
never offers it.
|
||||||
|
|
||||||
|
2. **"Set mit Preis" (menu) — per-block, irreversible mutation.** Applied server-side to the
|
||||||
|
cached `InvoiceDraftSession`, grouped by `ServiceRequestId`. For **every** service-request
|
||||||
|
block:
|
||||||
|
- Insert one dedicated, emphasised **set row** at the top of the block, carrying the block's
|
||||||
|
aggregated value (net + VAT + service-net/-VAT splits) as its price. This row is a real,
|
||||||
|
editable line item with its own id, so the user can manually change the set value afterwards
|
||||||
|
as an ordinary item edit.
|
||||||
|
- **Null out** the price of every existing item row in the block (set the price fields to
|
||||||
|
`null`, **not** `0`) so the row renders with an **empty** price/total cell. `null` and `0`
|
||||||
|
are semantically distinct here: `null` means "no price — render an empty cell and exclude
|
||||||
|
from the block sum", whereas `0` would legitimately print `0,00 €`. The rows themselves are
|
||||||
|
retained.
|
||||||
|
|
||||||
|
3. **"Nur Set mit Preis" (menu) — per-block, irreversible mutation.** As above, grouped by
|
||||||
|
`ServiceRequestId`. For every block:
|
||||||
|
- Insert the same dedicated, emphasised set row carrying the block's aggregated value.
|
||||||
|
- **Remove** every existing item row in the block from the dataset entirely (the lines are
|
||||||
|
gone, not merely hidden).
|
||||||
|
|
||||||
|
Properties common to the two menu modes (2 and 3):
|
||||||
|
|
||||||
|
- **Mutation, not display.** The change is written into `InvoiceDraftSession.Req` (the "cache
|
||||||
|
dataset") as a mutating `InvoiceDraftDelta`, computed on the server (never in the browser).
|
||||||
|
There is no render-time `admin.setmode` grouping flag driving how lines are shown, and no
|
||||||
|
reversible toggle.
|
||||||
|
- **Irreversible.** There is no patch to undo it. The only ways back are discarding the draft
|
||||||
|
(reloads the DB state) or hand-editing the resulting rows.
|
||||||
|
- **`SetItmId` is irrelevant.** Membership is the block, full stop.
|
||||||
|
- **Total unchanged.** The inserted set row's value equals the sum of the block's original items,
|
||||||
|
which are then excluded from the sum — either because their price is `null` (mode 2, `null`
|
||||||
|
counts as no contribution) or because they are gone (mode 3). So `InvoiceBalance`/
|
||||||
|
`InvoiceBalance_net` are unaffected.
|
||||||
|
- **Editor and PDF render identically** (ADR 0008): the dedicated set row is emphasised in both,
|
||||||
|
and both read the same mutated session.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- For the two menu modes, `InvoiceSetPricing` stops being a non-mutating render transform over
|
||||||
|
`type == "set"` groups; the grouping/insert/blank/remove is a real mutation in
|
||||||
|
`InvoiceDraftEditService`, keyed on the block. The inline `item.setprice` switch (operation 1)
|
||||||
|
remains the sole `SetItmId`-based, set-item-scoped operation.
|
||||||
|
- The previous `admin.setmode` display-flag model for these two modes — persisted `setmode:`
|
||||||
|
`InvoiceOptions` token, "Set-Preisanzeige menu entry disappears while unset", `Build(...)`
|
||||||
|
choosing `ShowPrice` per member at render time — is retired. Because the operation is a
|
||||||
|
one-shot irreversible mutation, there is no persisted display state to toggle. Any residual
|
||||||
|
`setmode:` token must degrade safely (ignored) and is no longer (re-)persisted.
|
||||||
|
- New/changed behaviour must be modelled as a server-side delta + calculator/service change and
|
||||||
|
covered by tests (`InvoiceDraftServiceTests`, `InvoiceSetPricingTests`): for each menu mode,
|
||||||
|
assert the inserted set row's value equals the block sum, the total is unchanged, mode 2
|
||||||
|
nulls-but-keeps member rows while mode 3 removes them, and an empty block is a no-op.
|
||||||
|
- Reminders follow the identical pattern when/if the same feature is offered there (ADR 0006/0008
|
||||||
|
reminder mirror).
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
Each of the following was raised and **explicitly decided against** as part of accepting this
|
||||||
|
decision — they are rejected choices, not open options to revisit without a superseding ADR:
|
||||||
|
|
||||||
|
- **Keep them as non-mutating display toggles** (the prior design): **explicitly rejected** by the
|
||||||
|
product owner — the set price must be a real, hand-editable value baked into the document, and
|
||||||
|
"Nur Set mit Preis" must actually drop the member lines, not just hide them.
|
||||||
|
- **Zero the blanked members' prices instead of nulling them**: **explicitly rejected** — `0` is
|
||||||
|
ambiguous (it prints `0,00 €`), so the frontend could not tell an empty cell from a genuine
|
||||||
|
zero price. Blanked members are set to `null` precisely to make "no price" unambiguous.
|
||||||
|
- **Carry the set price on the existing block heading row** instead of a dedicated row:
|
||||||
|
**explicitly rejected** — a separate, individually-editable set row keeps the section-heading
|
||||||
|
semantics intact and gives the user a concrete line to adjust afterwards.
|
||||||
|
- **Group by `SetItmId`/`type == "set"` headers like the inline switch:** **explicitly rejected**
|
||||||
|
— the menu modes present each *service request* as one set, independent of any mfr set-item;
|
||||||
|
conflating the two groupings is exactly the ambiguity this decision removes.
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
status: Accepted
|
||||||
|
date: 2026-07-15
|
||||||
|
applyTo:
|
||||||
|
- "Fuchs_DataService/**"
|
||||||
|
- "Fuchs/Services/PeriodicHostedService.cs"
|
||||||
|
- "Fuchs/Program.cs"
|
||||||
|
supersededBy: ""
|
||||||
|
---
|
||||||
|
|
||||||
|
# 0010 — MFR ERP sync runs in-process in the web app; Fuchs_DataService is a library
|
||||||
|
|
||||||
|
## Context
|
||||||
|
`Fuchs_DataService` was a standalone console/Windows Service hosted by **Topshelf**.
|
||||||
|
It carried its own `appsettings.json`, its own file-based configuration bootstrap
|
||||||
|
(`FdsConfig.Initialize()` reading the file), its own logging provider
|
||||||
|
(`FdsLoggerProvider`/`AddFdsLogging`), and a machine-name guard in `Main()` that
|
||||||
|
disabled the service on developer PCs. In practice the web app (`Fuchs`) already
|
||||||
|
referenced the project, already called `fds.FdsConfig.Initialize(builder.Configuration)`,
|
||||||
|
already registered `IFdsMfr`, and already created `FdsMfrClient` via
|
||||||
|
`IMfrClientFactory` — so the sync logic and the web app were sharing the same code
|
||||||
|
and the same connection strings while the service kept a second, parallel copy of
|
||||||
|
configuration/logging/hosting.
|
||||||
|
|
||||||
|
Maintaining a separate process, a second `appsettings.json` (duplicating connection
|
||||||
|
strings + MFR credentials), Topshelf, and a machine-name guard added drift risk and
|
||||||
|
operational overhead for no benefit the web host couldn't provide.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
- **`Fuchs_DataService` is now a class library** (no `OutputType Exe`, no Topshelf,
|
||||||
|
no own `appsettings.json`, no `install.bat`/`un-install.bat`, no
|
||||||
|
`System.Configuration.ConfigurationManager`). It contains only the MFR sync
|
||||||
|
logic (`FdsMfr`/`IFdsMfr`, `FdsMfrClient`), the DATEV/zip helpers, `FdsShared`,
|
||||||
|
`FdsDebug`, and `FdsConfig`.
|
||||||
|
- **The host owns configuration.** `FdsConfig` keeps only
|
||||||
|
`Initialize(IConfiguration)` (the file-based overload is gone). The Fuchs web app
|
||||||
|
injects its `IConfiguration`; connection strings (`fuchs_fds_ConnectionString`)
|
||||||
|
and MFR credentials (`Fds:MFR_*`, Key Vault-managed) come from Fuchs.
|
||||||
|
- **The host owns logging.** `FdsLoggerProvider`/`AddFdsLogging` were removed; the
|
||||||
|
library uses only `ILogger`/`ILoggerFactory` injected from Fuchs's logging
|
||||||
|
(`AddFuchsLogging`). The library depends only on
|
||||||
|
`Microsoft.Extensions.Logging.Abstractions` + `Microsoft.Extensions.Configuration.Binder`.
|
||||||
|
- **The sync runs in-process.** `PeriodicHostedService` (the generic
|
||||||
|
multi-job `BackgroundService`) moved to `Fuchs/Services/` and is registered in
|
||||||
|
`Program.cs` as a hosted service. The single `MfrSync` job calls
|
||||||
|
`UpdateIfNecessary_async` → `UpdateRequested_async` → `GetInvoiceFiles_async`.
|
||||||
|
- **A config flag replaces the machine-name guard.** Registration is gated by
|
||||||
|
`Fds:SyncEnabled` (default `false` when unset): `true` in production
|
||||||
|
`appsettings.json`, `false` in `appsettings.Development.json`, so developer
|
||||||
|
machines never poll the ERP. Interval (`Fds:ExecutionFrequency_Minutes`, default
|
||||||
|
15) and debug verbosity (`Fds:DebugDetails`) also come from the `Fds` section.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
- One process, one configuration surface, one logging pipeline. The sync inherits
|
||||||
|
the web app's OpenTelemetry, DI, and lifetime automatically.
|
||||||
|
- **Instance fan-out is a consideration:** the sync now runs in *every* web instance
|
||||||
|
where `Fds:SyncEnabled` is true. The intranet is deployed single-instance, so this
|
||||||
|
is acceptable; if Fuchs is ever scaled out, gate the sync to a single instance
|
||||||
|
(leader election / dedicated instance flag) to avoid concurrent MFR polling.
|
||||||
|
- Enabling/disabling the sync per environment is now a config change, not a
|
||||||
|
redeploy of a separate service.
|
||||||
|
- `Fuchs_DataService` is intentionally kept as a separate project (not folded into
|
||||||
|
`Fuchs`) so the sync logic stays isolated and unit-testable; `Fuchs.Tests` covers
|
||||||
|
it via `InternalsVisibleTo`.
|
||||||
|
- **The `Squid-Box.SevenZipSharp` native dependency (and the bundled `7z.dll`) was
|
||||||
|
removed** from both `Fuchs_DataService` and `Fuchs`. The only live archive use — the
|
||||||
|
DATEV export — is a plain, unencrypted zip, now produced via the native
|
||||||
|
`OCORE.zip.filesToZipArchive` (`System.IO.Compression`). The 7-Zip-only paths
|
||||||
|
(`.7z`/LZMA2, AES-encrypted archives, extraction, `FastAppend`) had no callers.
|
||||||
|
Trade-off accepted: `System.IO.Compression` cannot produce `.7z` or password/AES
|
||||||
|
archives; if that is ever required, a compression library must be reintroduced.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
- **Native `dotnet` Worker Service (separate process).** Would modernize off
|
||||||
|
Topshelf but keep the duplicate-config/duplicate-logging/second-process problem.
|
||||||
|
Rejected because the web app already hosts everything the sync needs.
|
||||||
|
- **Fold the code directly into `Fuchs`.** Rejected to preserve a clean, separately
|
||||||
|
testable sync library and avoid enlarging the web project.
|
||||||
@@ -94,8 +94,10 @@ payload; see `EVAL_live_invoice_editing.md` for the rationale.
|
|||||||
location/period (inline edit fields, `fm(...)` helper in `fis.inv_shared.js`).
|
location/period (inline edit fields, `fm(...)` helper in `fis.inv_shared.js`).
|
||||||
- **§13b reverse-charge** toggle (`$inv.sp13b`) — suppresses VAT lines/columns.
|
- **§13b reverse-charge** toggle (`$inv.sp13b`) — suppresses VAT lines/columns.
|
||||||
- **Set-pricing display mode** (`$inv.ssetmode` / `setSetmode`) — `SetPrice`
|
- **Set-pricing display mode** (`$inv.ssetmode` / `setSetmode`) — `SetPrice`
|
||||||
(default) / `ItemPrices` / `SetOnly`; see `INVOICE_SET_PRICING.md`. Purely
|
(default) / `SetOnly`; see `INVOICE_SET_PRICING.md`. Purely presentational
|
||||||
presentational — totals never change.
|
while the set header itself has no own price; once a set is converted via
|
||||||
|
the item switch (`item.setprice`), the conversion is one-way and totals
|
||||||
|
recompute from that point on.
|
||||||
- **Contact person** for the invoice (`$inv.sctp`, stored in `CustomValues`).
|
- **Contact person** for the invoice (`$inv.sctp`, stored in `CustomValues`).
|
||||||
|
|
||||||
All of this recalculates client-side totals live via the `fds.inv` event
|
All of this recalculates client-side totals live via the `fds.inv` event
|
||||||
@@ -122,7 +124,7 @@ On the server, `RegisterInvoiceAsync` (in `InvoiceService`) turns the posted
|
|||||||
JSON into SQL parameters and calls, in one batch:
|
JSON into SQL parameters and calls, in one batch:
|
||||||
- **New invoice**: `fds__createInvoice` (allocates the `Id`, returns a fresh
|
- **New invoice**: `fds__createInvoice` (allocates the `Id`, returns a fresh
|
||||||
row) → `fds__createInvoice_Details` (service net/VAT + `InvoiceOptions`,
|
row) → `fds__createInvoice_Details` (service net/VAT + `InvoiceOptions`,
|
||||||
e.g. `setmode:itemprices`, `§13b`).
|
e.g. `setmode:setonly`, `§13b`).
|
||||||
- **Existing draft**: `fds__setInvoice` (same parameter set, updates in place)
|
- **Existing draft**: `fds__setInvoice` (same parameter set, updates in place)
|
||||||
→ `fds__createInvoice_Details` again.
|
→ `fds__createInvoice_Details` again.
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,54 @@
|
|||||||
# Invoice "Set" Pricing — Design & Front-/Back-end Contract
|
# Invoice "Set" Pricing — Design & Front-/Back-end Contract
|
||||||
|
|
||||||
Customer requirement: items declared as a **set** in `[dbo].[mfr__items]`
|
> Governed by ADR [0009](Decisions/0009-block-setprice-modes-are-irreversible-mutations.md)
|
||||||
(`[Type] = 'set'`) should normally be shown as a single **set price** on the
|
> (the two menu modes) and ADR [0008](Decisions/0008-invoices-and-reminders-fully-backend-authoritative.md)
|
||||||
invoice instead of being broken up into their member items and summed.
|
> (everything server-side, PDF == editor). ADR 0009 **redefined** the two menu modes from the
|
||||||
|
> reversible, non-mutating display toggles this document previously described into irreversible,
|
||||||
|
> per-service-request-block mutations — the text below reflects the redefinition.
|
||||||
|
|
||||||
Three display modes (switchable in the invoice editor):
|
There are **three** separate set-price operations. They fall into two families that must not be
|
||||||
|
confused, because they group items by different keys and differ in whether they mutate the data:
|
||||||
|
|
||||||
| Mode | Set line | Member items | Use as |
|
| # | Operation | Trigger | Grouped by | Effect |
|
||||||
|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| **SetPrice** (default) | shown **with price** | shown **without price** | the new default |
|
| **1** | **Set-item switch** | row context button on a single `type == "set"` item (`$inv.toSetPrice`), shown only when it has a `SetItmId` | `SetItmId` (the mfr set-item and its members) | sums members onto the set header, sets the members' prices to `null` — **one-way mutation** |
|
||||||
| **ItemPrices** | shown as a heading **without price** | shown **with price** | the previous behaviour |
|
| **2** | **"Set mit Preis"** | editor menu ("Set-Preisanzeige") | **`ServiceRequestId`** (the whole block) | inserts a dedicated **set row** per block (block sum as its price) and sets every item's price to **`null`** (items shown **without price**) — **irreversible mutation** |
|
||||||
| **SetOnly** | shown **with price** | **removed** | compact |
|
| **3** | **"Nur Set mit Preis"** | editor menu ("Set-Preisanzeige") | **`ServiceRequestId`** (the whole block) | inserts the dedicated **set row** per block and **removes** every item line from the block — **irreversible mutation** |
|
||||||
|
|
||||||
> **Totals are unaffected.** The invoice total is taken from the registration
|
Only operation **1** reads `SetItmId`. Operations **2** and **3** ignore it entirely; their only
|
||||||
> balance (`InvoiceBalance` / `InvoiceBalance_net`), not by summing the rendered
|
grouping key is the service request (the editor tbody block). See "All set-price functions" below
|
||||||
> lines, so switching modes is purely presentational. The set price always
|
for the full breakdown.
|
||||||
> equals the sum of its members (computed as a fallback when the set header
|
|
||||||
> carries no own price).
|
|
||||||
|
|
||||||
## Back-end (implemented + unit-tested)
|
> **The two menu modes are irreversible data changes, not display toggles.** Choosing "Set mit
|
||||||
|
> Preis" or "Nur Set mit Preis" rewrites the block's items in the authoritative, server-cached
|
||||||
|
> draft session (`InvoiceDraftSession.Req` — the "cache dataset"): a dedicated set row is inserted
|
||||||
|
> and members have their price set to `null` (mode 2) or are deleted (mode 3). There is **no**
|
||||||
|
> reversible toggle and **no** persisted `admin.setmode` render flag driving grouping. The only
|
||||||
|
> ways back are discarding the draft (reloads the DB state) or hand-editing the resulting rows —
|
||||||
|
> the set row is a real, editable line item precisely so the user can adjust the set value
|
||||||
|
> afterwards.
|
||||||
|
|
||||||
|
> **`ItemPrices` / `admin.setmode` display-flag model was removed.** The earlier design persisted
|
||||||
|
> a `setmode:<mode>` token in `InvoiceOptions` and re-rendered set-item groups per that flag at
|
||||||
|
> render time (`InvoiceSetPricing.Build`). Under ADR 0009 the two menu modes are one-shot
|
||||||
|
> mutations, so there is no display state to persist or toggle. A stale `setmode:`/`itemprices`
|
||||||
|
> token degrades safely (ignored) and is never (re-)persisted.
|
||||||
|
|
||||||
|
> **Totals are unaffected.** The invoice total is taken from the registration balance
|
||||||
|
> (`InvoiceBalance` / `InvoiceBalance_net`), not by summing the rendered lines. Each operation
|
||||||
|
> conserves the total: the inserted set row's value equals the sum of the items it blanks (mode 2)
|
||||||
|
> or removes (mode 3), and the set-item switch (mode 1) writes exactly the members' sum onto the
|
||||||
|
> header.
|
||||||
|
|
||||||
|
## Back-end
|
||||||
|
|
||||||
|
> **Migration status (ADR 0009).** The code below still reflects the previous
|
||||||
|
> `admin.setmode` + `InvoiceSetPricing.Build` **display-mode** implementation for
|
||||||
|
> functions 2 & 3. Under ADR 0009 those two functions become per-block mutations in
|
||||||
|
> `InvoiceDraftEditService` (insert set row + blank/remove members); the render-time
|
||||||
|
> `Build`/`ModeFromInvoiceOptions`/`setmode` display path for them is being retired.
|
||||||
|
> Function 1 (`ApplyItemSetPrice`) is unaffected. Update this section as the
|
||||||
|
> migration lands so it stays a faithful description of the code.
|
||||||
|
|
||||||
- `Fuchs/code/InvoiceSetPricing.cs` — the authoritative transformation:
|
- `Fuchs/code/InvoiceSetPricing.cs` — the authoritative transformation:
|
||||||
`SetDisplayMode` + `Build(items, mode)` → ordered `InvoiceSetLine`s, each with
|
`SetDisplayMode` + `Build(items, mode)` → ordered `InvoiceSetLine`s, each with
|
||||||
@@ -34,15 +64,15 @@ Three display modes (switchable in the invoice editor):
|
|||||||
Wired in `Fuchs/js/intranet/modules/fis.inv_shared.js` (bundled to
|
Wired in `Fuchs/js/intranet/modules/fis.inv_shared.js` (bundled to
|
||||||
`wwwroot/web/fis.inv.de.js` via gulp `min:js`):
|
`wwwroot/web/fis.inv.de.js` via gulp `min:js`):
|
||||||
|
|
||||||
1. **Mode** — a 3-way switch (`$inv.ssetmode`, menu entry `setm`, label
|
1. **Menu modes** — the "Set-Preisanzeige" menu (`$inv.ssetmode` → `$inv.setSetmode`,
|
||||||
`$ict.setm`) writes the choice onto `admin.setmode`
|
menu entry `setm`, label `$ict.setm`) offers "Set mit Preis" and "Nur Set mit
|
||||||
(`setprice` | `itemprices` | `setonly`). The back-end
|
Preis". Selecting one posts a **mutating delta** (grouped by service-request
|
||||||
`FdsInvoiceData.BuildInvoiceOptions` turns that into the
|
block) to `inv/dpatch`; the server rewrites the block's items in the cached
|
||||||
`setmode:<mode>` token inside `@InvoiceOptions` (default `setprice` omitted),
|
session (inserts the set row, blanks or removes members) and pushes the new
|
||||||
persisted by `fds__createInvoice_Details` and read back by
|
state back via `draftReady`/`dstate`. The browser performs **no** grouping or
|
||||||
`InvoiceSetPricing.ModeFromInvoiceOptions`. This rides the **same `admin`
|
pricing math (ADR 0008/0009) — it only posts the chosen mode and re-renders the
|
||||||
channel as `§13b`** (the posted payload is `{admin, req, sms, new}` — `inv`
|
server's `req`/`sums`. There is no persisted `admin.setmode` display flag for
|
||||||
is not sent).
|
these two modes.
|
||||||
|
|
||||||
2. **Item shape** — `$inv.invSumUpdate` now posts each request block's
|
2. **Item shape** — `$inv.invSumUpdate` now posts each request block's
|
||||||
`items[]` in the back-end contract shape via `$inv.itemToContract`:
|
`items[]` in the back-end contract shape via `$inv.itemToContract`:
|
||||||
@@ -51,12 +81,20 @@ Wired in `Fuchs/js/intranet/modules/fis.inv_shared.js` (bundled to
|
|||||||
`FdsInvoiceData.InvoiceItems` does not read — so line items never reached the
|
`FdsInvoiceData.InvoiceItems` does not read — so line items never reached the
|
||||||
C# PDF. This change closes that gap for **all** invoices, not just sets.)
|
C# PDF. This change closes that gap for **all** invoices, not just sets.)
|
||||||
|
|
||||||
3. **Set flags** — `invSumUpdate` tags items as it builds `items[]`: an item with
|
3. **Set flags (function 1 only)** — `invSumUpdate` tags items as it builds `items[]`:
|
||||||
`type === 'set'` is a header (`id` = its set id); the **following items in the
|
an item with `type === 'set'` is a header (`id` = its set id); a member item's
|
||||||
same block become its members** (`setId` = the header's id) until the next set
|
`setId` is taken directly from the server-computed `SetItmId` field on the row
|
||||||
header. `mfr__items` has a `Type='set'` header but **no explicit member link**,
|
(`rrx.SetItmId`, populated by `fds__prepInvoice`'s `[SetItmID]` window function,
|
||||||
so this "header claims the following items in its block" rule is the convention
|
anchored on the still-unconverted, zero-priced `'set'` header that owns it) —
|
||||||
— adjust in `invSumUpdate` if mfr later exposes a real grouping.
|
**not** re-derived from row order in the browser. `mfr__items` itself still has no
|
||||||
|
explicit member link; `fds__prepInvoice` computes `SetItmId` per request from the
|
||||||
|
item list, so only items the server actually attributes to a set are tagged, and
|
||||||
|
unrelated items following a set in the list are never swept in. The header row's
|
||||||
|
own `SetItmId` self-references its own id (rather than being `null`); it is
|
||||||
|
explicitly excluded from being its own member both here (`sid !== citem.id`) and
|
||||||
|
in `InvoiceDraftEditService.ApplyItemSetPrice`. These flags feed **only** the
|
||||||
|
set-item switch (function 1); the two menu modes (functions 2 & 3) ignore
|
||||||
|
`SetItmId` and group by service-request block.
|
||||||
|
|
||||||
### Editor → backend field normalization (`$inv.invcPayload`)
|
### Editor → backend field normalization (`$inv.invcPayload`)
|
||||||
The editor's internal model keeps the long-standing key names, but the migrated C#
|
The editor's internal model keeps the long-standing key names, but the migrated C#
|
||||||
@@ -80,20 +118,95 @@ no per-rate `vat_*` keys.
|
|||||||
`sms.vat`, so non-19 % rates are stored correctly. Single-rate procs still store only
|
`sms.vat`, so non-19 % rates are stored correctly. Single-rate procs still store only
|
||||||
the highest rate.
|
the highest rate.
|
||||||
|
|
||||||
The editor's running **total stays the member sum in every mode**, matching the
|
The editor's running **total is unaffected by any set-price operation**, matching
|
||||||
registration balance — switching modes is purely presentational.
|
the registration balance — each operation conserves the total (the set row's value
|
||||||
|
equals the members it blanks/removes; the set-item switch writes exactly the
|
||||||
|
members' sum onto the header).
|
||||||
|
|
||||||
### Why the switch lives in the editor
|
### Why the trigger lives in the editor
|
||||||
Set grouping is only known where the request/item tree is rendered (front-end).
|
The choice of *when* to apply a set-price operation is only known where the invoice
|
||||||
The back-end intentionally stays the single, tested authority for *how* a chosen
|
is being composed (front-end), but the operation itself is executed **server-side**
|
||||||
mode maps to printed lines, so the editor only needs to pick the mode and tag the
|
against the cached draft session — the editor merely names the target (a set-item
|
||||||
items — it does not re-implement the pricing rules.
|
`Ref` for function 1, or the chosen menu mode for functions 2 & 3) and re-renders
|
||||||
|
the server's result. The back-end stays the single, tested authority for how each
|
||||||
|
operation rewrites the lines; the editor never re-implements the grouping, the
|
||||||
|
per-block aggregation, or the pricing rules (ADR 0008/0009).
|
||||||
|
|
||||||
|
## All set-price functions: before/after comparison
|
||||||
|
|
||||||
|
There are **three** distinct functions, and all three are **mutations** of the
|
||||||
|
authoritative cached draft session (`InvoiceDraftSession.Req`) — none is a
|
||||||
|
transient, freely-reversible view flag. They differ in what they group by and
|
||||||
|
what they touch:
|
||||||
|
|
||||||
|
- **Function 1 — the set-item switch** (`item.setprice`) groups by `SetItmId`
|
||||||
|
(one mfr set-item and its members) and is triggered per set row.
|
||||||
|
- **Functions 2 & 3 — the two menu modes** ("Set mit Preis" / "Nur Set mit
|
||||||
|
Preis") group by `ServiceRequestId` (the whole block), ignore `SetItmId`
|
||||||
|
entirely, and are triggered once from the "Set-Preisanzeige" menu.
|
||||||
|
|
||||||
|
Only function 1 reads `SetItmId`. All three are one-way; the only escape hatch
|
||||||
|
is discarding the draft or hand-editing the resulting rows.
|
||||||
|
|
||||||
|
### 1. The set-item switch (`item.setprice` patch, single set, mutating)
|
||||||
|
|
||||||
|
Triggered from the invoice editor's row context menu (`$inv.toSetPrice`), applied
|
||||||
|
server-side by `InvoiceDraftEditService.ApplyItemSetPrice`. The context button is
|
||||||
|
shown — and the operation available — **only** on a row that is `type == "set"`
|
||||||
|
**and** carries a `SetItmId` (and is still unconverted, own price `0`); a row
|
||||||
|
missing either condition never offers it. It gives a set-item its "own price": the
|
||||||
|
members' values are summed onto the header and the members' prices are set to
|
||||||
|
`null` (empty cell, excluded from the sum — not `0`). This
|
||||||
|
conversion is **one-way** — there is no patch to move a converted set back to
|
||||||
|
separately-priced members; the user would re-edit the individual line prices by
|
||||||
|
hand. It is the **only** function keyed on `SetItmId`.
|
||||||
|
|
||||||
|
| Aspect | Before the switch | After the switch |
|
||||||
|
|---|---|---|
|
||||||
|
| Set header item (`type == "set"`, `id == Ref`) price | `0` (zero-priced, as delivered by `fds__prepInvoice`) | Price fields (`total_net`/`v`/`vt` + VAT amounts `vv`/`vs`/`vsv`) replaced by the sum of all its members' corresponding values |
|
||||||
|
| Member items (`SetItmId == Ref`, excluding the header itself) | Each shows its own individual `total_net` / VAT amounts | Each price field is set to **`null`** (`v`/`vt`/`vv`/`vs`/`vsv` all `null`, not `0`) → renders an **empty** price/total cell and is excluded from the sum; the row itself stays in the list |
|
||||||
|
| Membership determination | N/A — membership already fixed by the server (`fds__prepInvoice`'s `[SetItmID]` window function) | **Unchanged** — the switch only sums/nulls the items the server already tagged; it never re-derives or reassigns `SetItmId` |
|
||||||
|
| Items **not** tagged with this header's `SetItmId` (e.g. unrelated items following the set in the same block) | Untouched | **Still untouched** — never swept in, regardless of row order/position |
|
||||||
|
| Draft version / history | — | Version bumped by one; an `item.setprice` history entry recorded with old/new header value |
|
||||||
|
| Invoice total (`Sums.TotalNet`/`TotalGross`) | Sum of all individual item prices (header 0 + each member's own price) | **Unchanged** — same total, because the header received exactly the sum of its members |
|
||||||
|
| Idempotency / no-ops | `Ref` unknown, or `Ref` does not point at a `type == 'set'` header → **no-op**: no version bump, no history entry | Same guard still applies after conversion — re-issuing the patch against a non-header `Ref` remains a no-op |
|
||||||
|
|
||||||
|
### 2 & 3. The two menu modes (per service-request block, mutating)
|
||||||
|
|
||||||
|
Triggered once from the editor's "Set-Preisanzeige" menu (`$inv.ssetmode` →
|
||||||
|
`$inv.setSetmode`) and applied server-side per **service-request block**
|
||||||
|
(`ServiceRequestId`), independent of any `type == "set"` item or `SetItmId`.
|
||||||
|
Both are **irreversible** and rewrite the block's items in the cached session.
|
||||||
|
|
||||||
|
For each block, a dedicated, emphasised **set row** is inserted (see "The
|
||||||
|
dedicated set row" below) carrying the block's aggregated value as its price;
|
||||||
|
then, depending on the mode, the block's original items are either blanked or
|
||||||
|
removed:
|
||||||
|
|
||||||
|
| Aspect | **"Set mit Preis"** (mode 2) | **"Nur Set mit Preis"** (mode 3) |
|
||||||
|
|---|---|---|
|
||||||
|
| Grouping key | `ServiceRequestId` (block) | `ServiceRequestId` (block) |
|
||||||
|
| Inserted set row | one per block, price = block's aggregated net (+ VAT/service splits) | one per block, same value |
|
||||||
|
| Original item rows | **kept**, but each price field (`v`/`vt`/`vv`/`vs`/`vsv`) is set to **`null`** (not `0`) → renders an **empty** price/total cell and is excluded from the block sum | **removed** from the block entirely |
|
||||||
|
| `SetItmId` | ignored | ignored |
|
||||||
|
| Reversibility | irreversible (discard draft or hand-edit) | irreversible (discard draft or hand-edit) |
|
||||||
|
| Invoice total | **unchanged** — the set row's value equals the sum of the block's members it blanks | **unchanged** — the set row's value equals the sum of the removed lines |
|
||||||
|
| Empty block | no-op | no-op |
|
||||||
|
|
||||||
|
### The dedicated set row
|
||||||
|
|
||||||
|
Both menu modes insert a **real, editable line item** (its own id, rendered
|
||||||
|
emphasised in the editor and the PDF), not a reused block-heading row and not a
|
||||||
|
render-only overlay. Because it is a genuine row in the cached dataset, the user
|
||||||
|
can adjust the set value afterwards with an ordinary item edit — that hand-edit
|
||||||
|
is the intended and only "undo" for the conversion (ADR
|
||||||
|
[0009](Decisions/0009-block-setprice-modes-are-irreversible-mutations.md)).
|
||||||
|
|
||||||
## Persistence note
|
## Persistence note
|
||||||
Draft/preview PDFs render straight from the posted `invc` JSON, so the contract
|
Draft/preview PDFs render straight from the cached draft session, so the contract
|
||||||
works end-to-end for previews and creation. `setmode` persists via
|
works end-to-end for previews and creation. The two menu modes bake their result
|
||||||
`InvoiceOptions`; the finalised document is rendered once and stored as a file, so
|
directly into the session's items (a set row plus blanked/removed members), so no
|
||||||
re-rendering from line items is not needed for correctness. Persisting the
|
`setmode:` display token is needed or persisted; the finalised document is
|
||||||
per-item `type`/`setId` flags (an SSDT + `fds__createInvoice_Details` change) is
|
rendered once and stored as a file. Persisting the per-item `type`/`setId` flags
|
||||||
only required if a finalised invoice must be **re-generated** from stored items in
|
(an SSDT + `fds__createInvoice_Details` change) is only required if a finalised
|
||||||
a different mode later — not done here.
|
invoice must be **re-generated** from stored items later — not done here.
|
||||||
|
|||||||
+3
-7
@@ -24,7 +24,6 @@
|
|||||||
<ProjectReference Include="..\CAMTParser\CAMTParser.csproj" />
|
<ProjectReference Include="..\CAMTParser\CAMTParser.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="code\7z.dll" CopyToOutputDirectory="PreserveNewest" />
|
|
||||||
<Content Include="Data\**" CopyToOutputDirectory="PreserveNewest" />
|
<Content Include="Data\**" CopyToOutputDirectory="PreserveNewest" />
|
||||||
<Content Include="favicon.ico" />
|
<Content Include="favicon.ico" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -45,7 +44,6 @@
|
|||||||
<PackageReference Include="PDFsharp" Version="6.2.4" />
|
<PackageReference Include="PDFsharp" Version="6.2.4" />
|
||||||
<PackageReference Include="PDFsharp-MigraDoc" Version="6.2.4" />
|
<PackageReference Include="PDFsharp-MigraDoc" Version="6.2.4" />
|
||||||
<PackageReference Include="Spire.PDF" Version="[8.10.5,8.10.5]" allowedVersions="[8.10.5,8.10.5]" />
|
<PackageReference Include="Spire.PDF" Version="[8.10.5,8.10.5]" allowedVersions="[8.10.5,8.10.5]" />
|
||||||
<PackageReference Include="Squid-Box.SevenZipSharp" Version="1.6.2.24" />
|
|
||||||
<!-- Updated packages -->
|
<!-- Updated packages -->
|
||||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
|
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
|
||||||
<PackageReference Include="MimeKit" Version="4.17.0" />
|
<PackageReference Include="MimeKit" Version="4.17.0" />
|
||||||
@@ -53,8 +51,8 @@
|
|||||||
<!-- New packages (needed for .NET 10) -->
|
<!-- New packages (needed for .NET 10) -->
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.2" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.2" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="4.0.0" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="4.0.0" />
|
||||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.9" />
|
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.10" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="10.0.9" />
|
<PackageReference Include="System.Drawing.Common" Version="10.0.10" />
|
||||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.29.1" />
|
<PackageReference Include="Azure.Storage.Blobs" Version="12.29.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -66,14 +64,12 @@
|
|||||||
|
|
||||||
|
|
||||||
<!-- UsingTask für plattformneutrales Zippen nach Publish -->
|
<!-- UsingTask für plattformneutrales Zippen nach Publish -->
|
||||||
<UsingTask TaskName="ZipDir" TaskFactory="CodeTaskFactory" AssemblyName="Microsoft.Build.Tasks.Core">
|
<UsingTask TaskName="ZipDir" TaskFactory="RoslynCodeTaskFactory" AssemblyName="Microsoft.Build.Tasks.Core">
|
||||||
<ParameterGroup>
|
<ParameterGroup>
|
||||||
<Source ParameterType="System.String" Required="true" />
|
<Source ParameterType="System.String" Required="true" />
|
||||||
<Destination ParameterType="System.String" Required="true" />
|
<Destination ParameterType="System.String" Required="true" />
|
||||||
</ParameterGroup>
|
</ParameterGroup>
|
||||||
<Task>
|
<Task>
|
||||||
<Reference Include="System.IO.Compression" />
|
|
||||||
<Reference Include="System.IO.Compression.FileSystem" />
|
|
||||||
<Code Type="Fragment" Language="cs">
|
<Code Type="Fragment" Language="cs">
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
if (System.IO.File.Exists(Destination)) {
|
if (System.IO.File.Exists(Destination)) {
|
||||||
|
|||||||
@@ -59,6 +59,31 @@ public class Program
|
|||||||
// FDS MFR singleton — ILogger<FdsMfr> and ILoggerFactory are supplied by the ASP.NET Core DI container
|
// FDS MFR singleton — ILogger<FdsMfr> and ILoggerFactory are supplied by the ASP.NET Core DI container
|
||||||
builder.Services.AddSingleton<fds.IFdsMfr, fds.FdsMfr>();
|
builder.Services.AddSingleton<fds.IFdsMfr, fds.FdsMfr>();
|
||||||
|
|
||||||
|
// In-process MFR ERP sync — formerly the standalone Fuchs_DataService Windows Service
|
||||||
|
// (Topshelf), now hosted here as a BackgroundService. Gated by Fds:SyncEnabled so
|
||||||
|
// developer machines (appsettings.Development.json sets it false) never poll the ERP;
|
||||||
|
// interval and debug verbosity come from the Fds config section.
|
||||||
|
if (builder.Configuration.GetValue("Fds:SyncEnabled", false))
|
||||||
|
{
|
||||||
|
builder.Services.AddHostedService(sp =>
|
||||||
|
{
|
||||||
|
var mfr = sp.GetRequiredService<fds.IFdsMfr>();
|
||||||
|
var interval = TimeSpan.FromMinutes(
|
||||||
|
builder.Configuration.GetValue("Fds:ExecutionFrequency_Minutes", 15d));
|
||||||
|
bool debug = builder.Configuration.GetValue("Fds:DebugDetails", false);
|
||||||
|
var jobs = new[]
|
||||||
|
{
|
||||||
|
new PeriodicJobDefinition("MfrSync", interval, async ct =>
|
||||||
|
{
|
||||||
|
await mfr.UpdateIfNecessary_async(debug, ct);
|
||||||
|
await mfr.UpdateRequested_async(debug, ct);
|
||||||
|
await mfr.GetInvoiceFiles_async(debug, ct);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
return new PeriodicHostedService(jobs, sp.GetRequiredService<ILogger<PeriodicHostedService>>());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// MVC with Razor view support
|
// MVC with Razor view support
|
||||||
builder.Services.AddControllersWithViews();
|
builder.Services.AddControllersWithViews();
|
||||||
builder.Services.AddSignalR();
|
builder.Services.AddSignalR();
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<DeleteExistingFiles>true</DeleteExistingFiles>
|
||||||
|
<ExcludeApp_Data>false</ExcludeApp_Data>
|
||||||
|
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
||||||
|
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
|
||||||
|
<LastUsedPlatform>x64</LastUsedPlatform>
|
||||||
|
<PublishProvider>FileSystem</PublishProvider>
|
||||||
|
<PublishUrl>Q:\PWProjects\Fuchs_Intranet</PublishUrl>
|
||||||
|
<WebPublishMethod>FileSystem</WebPublishMethod>
|
||||||
|
<_TargetId>Folder</_TargetId>
|
||||||
|
<SiteUrlToLaunchAfterPublish />
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
<ProjectGuid>2856176d-cda6-1be2-0ce0-d72c26fb4f35</ProjectGuid>
|
||||||
|
<SelfContained>false</SelfContained>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -116,6 +116,9 @@ public sealed class InvoiceDraftEditService : IInvoiceDraftService
|
|||||||
case "block.replace": return ReplaceBlock(s, d, ref oldValue, ref newValue);
|
case "block.replace": return ReplaceBlock(s, d, ref oldValue, ref newValue);
|
||||||
case "block.remove": return RemoveBlock(s, d, ref oldValue, ref newValue);
|
case "block.remove": return RemoveBlock(s, d, ref oldValue, ref newValue);
|
||||||
case "block.order": return ReorderBlocks(s, d, ref oldValue, ref newValue);
|
case "block.order": return ReorderBlocks(s, d, ref oldValue, ref newValue);
|
||||||
|
case "item.setprice": return ApplyItemSetPrice(s, d, ref oldValue, ref newValue);
|
||||||
|
case "block.setprice": return ApplyBlockSetPricing(s, removeMembers: false, ref oldValue, ref newValue);
|
||||||
|
case "block.setonly": return ApplyBlockSetPricing(s, removeMembers: true, ref oldValue, ref newValue);
|
||||||
default: return false;
|
default: return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,6 +188,183 @@ public sealed class InvoiceDraftEditService : IInvoiceDraftService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Auf Setpreis umstellen" (backend-authoritative, ADR 0006): irreversibly converts a Set
|
||||||
|
/// header item (<c>Ref</c> = the header's item id) from individually-priced members to a
|
||||||
|
/// single set price. Sums the <c>net</c>/<c>net_val</c>/<c>vat_val</c>/<c>svcnet_val</c>/
|
||||||
|
/// <c>svcvat_val</c> of every member line in the same block whose <c>SetItmId</c> equals the
|
||||||
|
/// header id (the grouping <c>fds__prepInvoice</c> computes, anchored on the still-unconverted,
|
||||||
|
/// zero-priced Set header — see the <c>[SetItmID]</c> window function there), writes that sum
|
||||||
|
/// onto the header line and zeroes each member's price fields — matching the visual
|
||||||
|
/// <c>SetPrice</c> display mode (<see cref="InvoiceSetPricing"/>), but as an actual, irreversible
|
||||||
|
/// data change rather than a display toggle. The user can still change the resulting header price
|
||||||
|
/// manually afterwards (ordinary item edit), which is why this is one-way.
|
||||||
|
/// <para>
|
||||||
|
/// The header row's own <c>SetItmId</c> now self-references its own id (rather than being
|
||||||
|
/// <c>null</c>) once <c>fds__prepInvoice</c> stopped special-casing it, so membership is
|
||||||
|
/// determined by <c>id == Ref</c> first (captured as <paramref name="header"/> below and
|
||||||
|
/// <c>continue</c>d past) — a header is never mistaken for its own member.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
private static bool ApplyItemSetPrice(InvoiceDraftSession s, InvoiceDraftDelta d, ref string oldValue, ref string newValue)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(d.Ref)) return false;
|
||||||
|
foreach (var blockTok in s.Req)
|
||||||
|
{
|
||||||
|
if (blockTok is not JObject block || block["itm"] is not JArray lines) continue;
|
||||||
|
JObject? header = null;
|
||||||
|
var members = new List<JObject>();
|
||||||
|
foreach (var lineTok in lines)
|
||||||
|
{
|
||||||
|
if (lineTok is not JObject co) continue;
|
||||||
|
if (Str(co["id"]) == d.Ref) { header = co; continue; } // the header itself is never a member, even if its SetItmId self-references
|
||||||
|
if (Str(co["SetItmId"]) == d.Ref) members.Add(co);
|
||||||
|
}
|
||||||
|
if (header == null) continue;
|
||||||
|
if (!string.Equals(Str(header["typ"]), "set", StringComparison.OrdinalIgnoreCase)) return false;
|
||||||
|
|
||||||
|
oldValue = InvoiceDraftCalculator.Dec(header["vt"]).ToString(CultureInfo.InvariantCulture);
|
||||||
|
decimal sumNet = 0, sumVat = 0, sumSvcNet = 0, sumSvcVat = 0;
|
||||||
|
foreach (var m in members)
|
||||||
|
{
|
||||||
|
sumNet += InvoiceDraftCalculator.Dec(m["vt"]);
|
||||||
|
sumVat += InvoiceDraftCalculator.Dec(m["vv"]);
|
||||||
|
sumSvcNet += InvoiceDraftCalculator.Dec(m["vs"]);
|
||||||
|
sumSvcVat += InvoiceDraftCalculator.Dec(m["vsv"]);
|
||||||
|
NullLinePrice(m); // ADR 0009: null (empty cell), not 0 — excluded from the sum, distinct from a real 0,00 €
|
||||||
|
}
|
||||||
|
header["v"] = sumNet; header["vt"] = sumNet; header["vv"] = sumVat; header["vs"] = sumSvcNet; header["vsv"] = sumSvcVat;
|
||||||
|
newValue = sumNet.ToString(CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
// Mirror the conversion onto the "items" contract shape (InvoiceSetPricing/BuildSetDisplay
|
||||||
|
// read total_net from here, not from "itm") so the online editor's set-display immediately
|
||||||
|
// reflects that the set is now converted, instead of waiting for a reload.
|
||||||
|
if (block["items"] is JArray items)
|
||||||
|
{
|
||||||
|
foreach (var itemTok in items)
|
||||||
|
{
|
||||||
|
if (itemTok is not JObject io) continue;
|
||||||
|
string id = Str(io["id"]);
|
||||||
|
if (id == d.Ref) { io["total_net"] = sumNet; }
|
||||||
|
else if (members.Any(m => Str(m["id"]) == id)) { NullItemPrice(io); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The two menu set-price modes (ADR 0009), grouped by service-request <b>block</b>
|
||||||
|
/// (not <c>SetItmId</c>) and applied server-side as an <b>irreversible mutation</b> of the
|
||||||
|
/// cached session — never a display flag. For every block that carries any priced line:
|
||||||
|
/// a dedicated, emphasised set row (<c>typ == "set"</c>) is inserted at the top carrying the
|
||||||
|
/// block's aggregated value (net + VAT + service-net/-VAT splits); then the block's original
|
||||||
|
/// items are either price-<b>nulled</b> (<paramref name="removeMembers"/> = false → "Set mit
|
||||||
|
/// Preis": empty price cells, kept) or <b>removed</b> (<paramref name="removeMembers"/> = true →
|
||||||
|
/// "Nur Set mit Preis"). <c>null</c> (not <c>0</c>) marks "no price" — an empty cell excluded
|
||||||
|
/// from the sum, distinct from a genuine 0,00 €. Any pre-existing set membership is neutralised
|
||||||
|
/// (existing set rows demoted, <c>setId</c>/<c>SetItmId</c> cleared) so the block becomes one
|
||||||
|
/// flat set and the two groupings (this vs. the <c>SetItmId</c>-based item switch) never collide.
|
||||||
|
/// The invoice total is conserved: the set row's value equals the sum of the members it blanks
|
||||||
|
/// or removes.
|
||||||
|
/// </summary>
|
||||||
|
private static bool ApplyBlockSetPricing(InvoiceDraftSession s, bool removeMembers, ref string oldValue, ref string newValue)
|
||||||
|
{
|
||||||
|
bool mutated = false;
|
||||||
|
int converted = 0;
|
||||||
|
foreach (var blockTok in s.Req)
|
||||||
|
{
|
||||||
|
if (blockTok is not JObject block || block["itm"] is not JArray lines || lines.Count == 0) continue;
|
||||||
|
|
||||||
|
// Aggregate the block from its current line values (nulled/absent = 0 contribution).
|
||||||
|
decimal net = 0, vat = 0, svcNet = 0, svcVat = 0;
|
||||||
|
string rate = "";
|
||||||
|
foreach (var lineTok in lines)
|
||||||
|
{
|
||||||
|
if (lineTok is not JObject co) continue;
|
||||||
|
net += InvoiceDraftCalculator.Dec(co["vt"]);
|
||||||
|
vat += InvoiceDraftCalculator.Dec(co["vv"]);
|
||||||
|
svcNet += InvoiceDraftCalculator.Dec(co["vs"]);
|
||||||
|
svcVat += InvoiceDraftCalculator.Dec(co["vsv"]);
|
||||||
|
string r = InvoiceDraftCalculator.NormalizeRate(Str(co["vat"]));
|
||||||
|
if (r.Length > 0) rate = r; // collapsed set line carries the block's (highest/last) rate
|
||||||
|
}
|
||||||
|
if (net == 0 && vat == 0 && svcNet == 0 && svcVat == 0) continue; // nothing priced -> skip block
|
||||||
|
|
||||||
|
string blockId = Str(block["Id"]);
|
||||||
|
string setId = "bset_" + (blockId.Length > 0 ? blockId : Guid.NewGuid().ToString("N")[..8]);
|
||||||
|
const string title = "Gesamtumfang pauschal"; // neutral label for the block set row (ADR 0009) — not the service-request title
|
||||||
|
|
||||||
|
// The dedicated, emphasised set row, in both line shapes (itm = editor co; items = contract).
|
||||||
|
var setItm = new JObject
|
||||||
|
{
|
||||||
|
["id"] = setId, ["typ"] = "set", ["p"] = "", ["q"] = "", ["t"] = title, ["tt"] = "",
|
||||||
|
["v"] = JValue.CreateNull(), ["vt"] = net, ["vv"] = vat, ["vs"] = svcNet, ["vsv"] = svcVat,
|
||||||
|
["vat"] = rate, ["det"] = false
|
||||||
|
};
|
||||||
|
var setItem = new JObject
|
||||||
|
{
|
||||||
|
["id"] = setId, ["type"] = "set", ["title"] = title, ["desc"] = "", ["qty"] = "",
|
||||||
|
["price_net"] = net, ["total_net"] = net, ["vat"] = rate, ["setId"] = JValue.CreateNull()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mutate the itm array in place (avoids Newtonsoft re-parenting): drop members (setonly)
|
||||||
|
// or null their prices + neutralise set membership (setprice), then prepend the set row.
|
||||||
|
if (removeMembers) lines.Clear();
|
||||||
|
else
|
||||||
|
foreach (var lineTok in lines)
|
||||||
|
{
|
||||||
|
if (lineTok is not JObject co) continue;
|
||||||
|
DemoteSet(co, "typ"); co["SetItmId"] = JValue.CreateNull();
|
||||||
|
NullLinePrice(co);
|
||||||
|
}
|
||||||
|
lines.Insert(0, setItm);
|
||||||
|
|
||||||
|
if (block["items"] is JArray items)
|
||||||
|
{
|
||||||
|
if (removeMembers) items.Clear();
|
||||||
|
else
|
||||||
|
foreach (var itemTok in items)
|
||||||
|
{
|
||||||
|
if (itemTok is not JObject io) continue;
|
||||||
|
DemoteSet(io, "type"); io["setId"] = JValue.CreateNull();
|
||||||
|
NullItemPrice(io);
|
||||||
|
}
|
||||||
|
items.Insert(0, setItem);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
block["items"] = new JArray { setItem };
|
||||||
|
}
|
||||||
|
|
||||||
|
mutated = true; converted++;
|
||||||
|
}
|
||||||
|
oldValue = "";
|
||||||
|
newValue = (removeMembers ? "setonly" : "setprice") + ":" + converted;
|
||||||
|
return mutated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Demotes a pre-existing set row to a plain line so only the freshly inserted block set row stays "set".</summary>
|
||||||
|
private static void DemoteSet(JObject line, string typeKey)
|
||||||
|
{
|
||||||
|
if (line[typeKey] is { } t && string.Equals(Str(t), "set", StringComparison.OrdinalIgnoreCase))
|
||||||
|
line[typeKey] = "other";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Sets the editor <c>co</c> price fields to JSON null (empty cell, excluded from sums — ADR 0009).</summary>
|
||||||
|
private static void NullLinePrice(JObject co)
|
||||||
|
{
|
||||||
|
co["v"] = JValue.CreateNull(); co["vt"] = JValue.CreateNull(); co["vv"] = JValue.CreateNull();
|
||||||
|
co["vs"] = JValue.CreateNull(); co["vsv"] = JValue.CreateNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Sets the items-contract price fields to JSON null (empty cell — ADR 0009).</summary>
|
||||||
|
private static void NullItemPrice(JObject io)
|
||||||
|
{
|
||||||
|
io["price_net"] = JValue.CreateNull(); io["total_net"] = JValue.CreateNull();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reorders the service-request blocks to the id sequence the editor posts after a section
|
/// Reorders the service-request blocks to the id sequence the editor posts after a section
|
||||||
/// drag (<c>Value</c> = ["id",…]). Named ids move into the given order; any not named are kept
|
/// drag (<c>Value</c> = ["id",…]). Named ids move into the given order; any not named are kept
|
||||||
@@ -269,7 +449,9 @@ public sealed class InvoiceDraftEditService : IInvoiceDraftService
|
|||||||
{
|
{
|
||||||
if (blockTok is not JObject block || block["items"] is not JArray itemsArr) continue;
|
if (blockTok is not JObject block || block["items"] is not JArray itemsArr) continue;
|
||||||
List<Dictionary<string, object?>>? items = itemsArr.ToObject<List<Dictionary<string, object?>>>();
|
List<Dictionary<string, object?>>? items = itemsArr.ToObject<List<Dictionary<string, object?>>>();
|
||||||
if (items == null || !InvoiceSetPricing.ContainsSets(items)) continue;
|
// Only function-1 (SetItmId-linked) sets get display flags; block-mode sets (ADR 0009)
|
||||||
|
// clear setId and render flat, so BuildSetDisplay must not sweep them into Build here.
|
||||||
|
if (items == null || !InvoiceSetPricing.HasSetMembers(items)) continue;
|
||||||
|
|
||||||
foreach (var line in InvoiceSetPricing.Build(items, mode))
|
foreach (var line in InvoiceSetPricing.Build(items, mode))
|
||||||
{
|
{
|
||||||
@@ -331,6 +513,7 @@ public sealed class InvoiceDraftEditService : IInvoiceDraftService
|
|||||||
// ── Internals ──────────────────────────────────────────────────────────────
|
// ── Internals ──────────────────────────────────────────────────────────────
|
||||||
private static void Refresh(InvoiceDraftSession session)
|
private static void Refresh(InvoiceDraftSession session)
|
||||||
{
|
{
|
||||||
|
InvoiceDraftCalculator.RecomputeLineValues(session);
|
||||||
InvoiceDraftCalculator.RecomputeTotals(session);
|
InvoiceDraftCalculator.RecomputeTotals(session);
|
||||||
InvoiceDraftCalculator.RecomputePositions(session);
|
InvoiceDraftCalculator.RecomputePositions(session);
|
||||||
InvoiceDraftCalculator.Validate(session);
|
InvoiceDraftCalculator.Validate(session);
|
||||||
@@ -430,7 +613,7 @@ public sealed class InvoiceDraftEditService : IInvoiceDraftService
|
|||||||
string setmode = Str(session.Admin["setmode"]).Trim().ToLowerInvariant();
|
string setmode = Str(session.Admin["setmode"]).Trim().ToLowerInvariant();
|
||||||
// Mirrors FdsInvoiceData.BuildInvoiceOptions: persist any explicitly-chosen mode
|
// Mirrors FdsInvoiceData.BuildInvoiceOptions: persist any explicitly-chosen mode
|
||||||
// (including the default "setprice") so it is distinguishable from "never touched".
|
// (including the default "setprice") so it is distinguishable from "never touched".
|
||||||
if (setmode is "setprice" or "itemprices" or "setonly") tokens.Add("setmode:" + setmode);
|
if (setmode is "setprice" or "setonly") tokens.Add("setmode:" + setmode);
|
||||||
return string.Join(",", tokens);
|
return string.Join(",", tokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -1,7 +1,7 @@
|
|||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace fds;
|
namespace Fuchs.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines a named job with its own execution schedule for use with <see cref="PeriodicHostedService"/>.
|
/// Defines a named job with its own execution schedule for use with <see cref="PeriodicHostedService"/>.
|
||||||
@@ -13,6 +13,8 @@ public sealed record PeriodicJobDefinition(
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A <see cref="BackgroundService"/> that runs multiple independent jobs, each on its own <see cref="PeriodicTimer"/>.
|
/// A <see cref="BackgroundService"/> that runs multiple independent jobs, each on its own <see cref="PeriodicTimer"/>.
|
||||||
|
/// Hosts the MFR ERP sync that formerly ran as the standalone <c>Fuchs_DataService</c> Windows Service
|
||||||
|
/// (Topshelf); it is now registered in-process from <c>Program.cs</c>, gated by <c>Fds:SyncEnabled</c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PeriodicHostedService : BackgroundService
|
public sealed class PeriodicHostedService : BackgroundService
|
||||||
{
|
{
|
||||||
@@ -19,6 +19,10 @@
|
|||||||
string appName = ViewData["AppName"] as string ?? "Fuchs Intranet";
|
string appName = ViewData["AppName"] as string ?? "Fuchs Intranet";
|
||||||
string fullName = ViewData["FullName"] as string ?? "";
|
string fullName = ViewData["FullName"] as string ?? "";
|
||||||
string pageTitle = ViewData["Title"] as string ?? "Intranet";
|
string pageTitle = ViewData["Title"] as string ?? "Intranet";
|
||||||
|
// When the deployment is flagged as a test/staging instance (Fuchs:IsTestDeployment = true),
|
||||||
|
// a distinctive green background is injected below so a test instance is never mistaken for
|
||||||
|
// production. Defaults to false (production) in appsettings.json.
|
||||||
|
bool isTestDeployment = Configuration.GetValue<bool>("Fuchs:IsTestDeployment");
|
||||||
string? debugDbTarget = null;
|
string? debugDbTarget = null;
|
||||||
if (HostEnvironment.IsDevelopment())
|
if (HostEnvironment.IsDevelopment())
|
||||||
{
|
{
|
||||||
@@ -56,6 +60,15 @@
|
|||||||
|
|
||||||
@await RenderSectionAsync("CustomHeader", required: false)
|
@await RenderSectionAsync("CustomHeader", required: false)
|
||||||
|
|
||||||
|
@if (isTestDeployment)
|
||||||
|
{
|
||||||
|
<style>
|
||||||
|
body, html, body > header > nav, body > main > #topbar > nav {
|
||||||
|
background-color: #13a143 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
}
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$ocms.auth = @Html.Raw(JsonConvert.SerializeObject(authinfo));
|
$ocms.auth = @Html.Raw(JsonConvert.SerializeObject(authinfo));
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -39,6 +39,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Fds": {
|
"Fds": {
|
||||||
|
"SyncEnabled": false,
|
||||||
|
"ExecutionFrequency_Minutes": 15,
|
||||||
|
"DebugDetails": true,
|
||||||
"MFR_host": "portal.mobilefieldreport.com",
|
"MFR_host": "portal.mobilefieldreport.com",
|
||||||
"MFR_UserName": "system@sebastian-fuchs---bad-und-heizung-gmbh-und-co-kg.com",
|
"MFR_UserName": "system@sebastian-fuchs---bad-und-heizung-gmbh-und-co-kg.com",
|
||||||
"MFR_Password": "0oT4G3H2"
|
"MFR_Password": "0oT4G3H2"
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"AzureBlobStorage_ConnectionString": "MANAGED_BY_KEYVAULT"
|
"AzureBlobStorage_ConnectionString": "MANAGED_BY_KEYVAULT"
|
||||||
},
|
},
|
||||||
"Fuchs": {
|
"Fuchs": {
|
||||||
|
"IsTestDeployment": true,
|
||||||
"ocms_guid": "00094b8f-a822-4e9c-b627-87802f93fca8",
|
"ocms_guid": "00094b8f-a822-4e9c-b627-87802f93fca8",
|
||||||
"ocms_valid_locales_csv": "de,en",
|
"ocms_valid_locales_csv": "de,en",
|
||||||
"ocms_default_locale": "de",
|
"ocms_default_locale": "de",
|
||||||
@@ -48,7 +49,7 @@
|
|||||||
"Enabled": false
|
"Enabled": false
|
||||||
},
|
},
|
||||||
"Email": {
|
"Email": {
|
||||||
"OverrideRecipient": ""
|
"OverrideRecipient": "info@processweb.de"
|
||||||
},
|
},
|
||||||
"StartupChecks": {
|
"StartupChecks": {
|
||||||
"Enabled": false,
|
"Enabled": false,
|
||||||
@@ -60,9 +61,9 @@
|
|||||||
"StartupEmailRecipientName": "Startup Monitor"
|
"StartupEmailRecipientName": "Startup Monitor"
|
||||||
},
|
},
|
||||||
"AzureStorage": {
|
"AzureStorage": {
|
||||||
"Enabled": false,
|
"Enabled": true,
|
||||||
"InvoiceContainer": "fuchs-invoices",
|
"InvoiceContainer": "dev-fuchs-invoices",
|
||||||
"ReminderContainer": "fuchs-reminders",
|
"ReminderContainer": "dev-fuchs-reminders",
|
||||||
"MetadataFields": [ "Id", "Version", "InvoiceId", "InvoiceTitle", "InvId", "DocumentName", "file_guid" ]
|
"MetadataFields": [ "Id", "Version", "InvoiceId", "InvoiceTitle", "InvId", "DocumentName", "file_guid" ]
|
||||||
},
|
},
|
||||||
"Telemetry": {
|
"Telemetry": {
|
||||||
@@ -71,6 +72,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Fds": {
|
"Fds": {
|
||||||
|
"SyncEnabled": true,
|
||||||
|
"ExecutionFrequency_Minutes": 15,
|
||||||
|
"DebugDetails": false,
|
||||||
"MFR_host": "portal.mobilefieldreport.com",
|
"MFR_host": "portal.mobilefieldreport.com",
|
||||||
"MFR_UserName": "MANAGED_BY_KEYVAULT",
|
"MFR_UserName": "MANAGED_BY_KEYVAULT",
|
||||||
"MFR_Password": "MANAGED_BY_KEYVAULT"
|
"MFR_Password": "MANAGED_BY_KEYVAULT"
|
||||||
|
|||||||
Binary file not shown.
@@ -175,7 +175,7 @@ public class FdsInvoiceData
|
|||||||
// explicit user choice of the default is indistinguishable from an invoice that was never
|
// explicit user choice of the default is indistinguishable from an invoice that was never
|
||||||
// switched to set-pricing at all (needed so the editor can hide the "Set-Preisanzeige"
|
// switched to set-pricing at all (needed so the editor can hide the "Set-Preisanzeige"
|
||||||
// menu entry once a mode has been chosen; see InvoiceDraftEditService.BuildInvoiceOptions).
|
// menu entry once a mode has been chosen; see InvoiceDraftEditService.BuildInvoiceOptions).
|
||||||
if (setmode is "setprice" or "itemprices" or "setonly") tokens.Add("setmode:" + setmode);
|
if (setmode is "setprice" or "setonly") tokens.Add("setmode:" + setmode);
|
||||||
return string.Join(",", tokens);
|
return string.Join(",", tokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-6
@@ -168,6 +168,9 @@ public static class FuchsPdf
|
|||||||
bool isText = type is "text" or "title";
|
bool isText = type is "text" or "title";
|
||||||
ParseDec(i.no("price_net", 0), out decimal price);
|
ParseDec(i.no("price_net", 0), out decimal price);
|
||||||
ParseDec(i.no("total_net", 0), out decimal total);
|
ParseDec(i.no("total_net", 0), out decimal total);
|
||||||
|
// ADR 0009: a null/absent total_net means "no price" — render an empty cell (not 0,00 €),
|
||||||
|
// distinct from a genuine 0. This is how block-mode (Set mit Preis) nulled members print.
|
||||||
|
bool hasPrice = i.TryGetValue("total_net", out var tv) && tv != null;
|
||||||
return new InvoiceSetLine
|
return new InvoiceSetLine
|
||||||
{
|
{
|
||||||
Title = i.nz("title", ""),
|
Title = i.nz("title", ""),
|
||||||
@@ -175,7 +178,8 @@ public static class FuchsPdf
|
|||||||
Qty = i.nz("qty", ""),
|
Qty = i.nz("qty", ""),
|
||||||
PriceNet = price,
|
PriceNet = price,
|
||||||
TotalNet = total,
|
TotalNet = total,
|
||||||
ShowPrice = !isText,
|
ShowPrice = !isText && hasPrice,
|
||||||
|
Numbered = !isText, // numbered like the editor: every line except free-text/heading, even price-blanked set members
|
||||||
IsSetHeader = type == "set"
|
IsSetHeader = type == "set"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -764,8 +768,8 @@ public static class FuchsPdf
|
|||||||
// Each section prints its heading; positions are numbered the same way the editor numbers
|
// Each section prints its heading; positions are numbered the same way the editor numbers
|
||||||
// them (every line except free-text/heading lines, including a set header). The chosen
|
// them (every line except free-text/heading lines, including a set header). The chosen
|
||||||
// set-display mode (see INVOICE_SET_PRICING.md) governs how "set" items and their members
|
// set-display mode (see INVOICE_SET_PRICING.md) governs how "set" items and their members
|
||||||
// are shown: SetPrice (default) prices the set, blanks members; ItemPrices prices the
|
// are shown: SetPrice (default) prices the set, blanks members; SetOnly prices the set
|
||||||
// members, blanks the set heading; SetOnly prices the set and drops the members entirely.
|
// and drops the members entirely.
|
||||||
// Blocks without any set items are unaffected and always render flat.
|
// Blocks without any set items are unaffected and always render flat.
|
||||||
var setMode = InvoiceSetPricing.ModeFromInvoiceOptions(inv.InvoiceRegistration?.getString("InvoiceOptions"));
|
var setMode = InvoiceSetPricing.ModeFromInvoiceOptions(inv.InvoiceRegistration?.getString("InvoiceOptions"));
|
||||||
int pos = 0;
|
int pos = 0;
|
||||||
@@ -779,13 +783,16 @@ public static class FuchsPdf
|
|||||||
hr.Cells[1].AddParagraph().WithStyle("TblCell_RTitle").AddFormattedText(block.Heading, TextFormat.Bold);
|
hr.Cells[1].AddParagraph().WithStyle("TblCell_RTitle").AddFormattedText(block.Heading, TextFormat.Bold);
|
||||||
}
|
}
|
||||||
|
|
||||||
var lines = InvoiceSetPricing.ContainsSets(block.Items)
|
// Function-1 (SetItmId-linked) sets still collapse via Build; block-mode sets (ADR 0009)
|
||||||
|
// and plain blocks render flat — their set row is emphasised on its own and nulled
|
||||||
|
// members map to blank cells (see MapItemToLine).
|
||||||
|
var lines = InvoiceSetPricing.HasSetMembers(block.Items)
|
||||||
? InvoiceSetPricing.Build(block.Items, setMode)
|
? InvoiceSetPricing.Build(block.Items, setMode)
|
||||||
: block.Items.Select(MapItemToLine).ToList(); // no sets: flat, faithful mirror of the editor
|
: block.Items.Select(MapItemToLine).ToList();
|
||||||
|
|
||||||
foreach (var line in lines)
|
foreach (var line in lines)
|
||||||
{
|
{
|
||||||
bool numbered = line.IsSetHeader || line.ShowPrice; // free-text/heading lines carry no number
|
bool numbered = line.Numbered; // every line except free-text/heading (mirrors the editor's RecomputePositions), even price-blanked set members
|
||||||
var row = tbl.AddRow();
|
var row = tbl.AddRow();
|
||||||
row.HeightRule = RowHeightRule.Auto;
|
row.HeightRule = RowHeightRule.Auto;
|
||||||
row.Cells[0].AddParagraph(numbered ? (++pos).ToString() : "").Style = "TblCell_Base";
|
row.Cells[0].AddParagraph(numbered ? (++pos).ToString() : "").Style = "TblCell_Base";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace Fuchs.intranet;
|
namespace Fuchs.intranet;
|
||||||
@@ -16,6 +16,45 @@ namespace Fuchs.intranet;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class InvoiceDraftCalculator
|
public static class InvoiceDraftCalculator
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Recomputes every line's own net/VAT/service values from its raw quantity (<c>qn</c>),
|
||||||
|
/// unit price (<c>v</c>) and VAT rate (<c>vat</c>) — the authoritative, server-side port of
|
||||||
|
/// the former client-side <c>quantChange</c>/<c>setVat</c> multiplication (ADR 0006/0008: the
|
||||||
|
/// online editor performs no arithmetic at all, not even a single line's net = qty × price).
|
||||||
|
/// Only lines that actually carry a raw quantity and a positive unit price are recomputed
|
||||||
|
/// (mirrors <c>quantChange</c>'s own guard); lines without both (headings, free text,
|
||||||
|
/// combined-sum rows, and set members that have been zeroed by
|
||||||
|
/// <see cref="Fuchs.Services.InvoiceDraftEditService"/>'s "Auf Setpreis umstellen" conversion)
|
||||||
|
/// keep whatever value they already carry, so a set header's synthesised sum is never
|
||||||
|
/// clobbered by a subsequent recompute.
|
||||||
|
/// </summary>
|
||||||
|
public static void RecomputeLineValues(InvoiceDraftSession session)
|
||||||
|
{
|
||||||
|
foreach (var blockTok in session.Req)
|
||||||
|
{
|
||||||
|
if (blockTok is not JObject block || block["itm"] is not JArray lines) continue;
|
||||||
|
foreach (var lineTok in lines)
|
||||||
|
{
|
||||||
|
if (lineTok is not JObject co) continue;
|
||||||
|
decimal qty = Dec(co["qn"]);
|
||||||
|
decimal price = Dec(co["v"]);
|
||||||
|
if (qty <= 0 || price <= 0) continue; // no raw qty/price posted -> leave the value as delivered
|
||||||
|
|
||||||
|
string rate = NormalizeRate(Str(co["vat"]));
|
||||||
|
decimal vatFactor = rate.Length > 0 && decimal.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal r)
|
||||||
|
? r / 100m : 0m;
|
||||||
|
decimal netVal = Math.Round(qty * price, 2, MidpointRounding.AwayFromZero);
|
||||||
|
decimal vatVal = Math.Round(netVal * vatFactor, 2, MidpointRounding.AwayFromZero);
|
||||||
|
|
||||||
|
co["vt"] = netVal;
|
||||||
|
co["vv"] = vatVal;
|
||||||
|
bool isService = string.Equals(Str(co["typ"]), "Service", StringComparison.OrdinalIgnoreCase);
|
||||||
|
co["vs"] = isService ? netVal : 0;
|
||||||
|
co["vsv"] = isService ? vatVal : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Aggregates every block's line values into the draft's totals — the port of
|
/// Aggregates every block's line values into the draft's totals — the port of
|
||||||
/// <c>invSumUpdate</c>'s <c>csms</c> accumulation plus §13b (VAT suppressed → gross = net).
|
/// <c>invSumUpdate</c>'s <c>csms</c> accumulation plus §13b (VAT suppressed → gross = net).
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ public enum SetDisplayMode
|
|||||||
{
|
{
|
||||||
/// <summary>Default: show the set as one priced line; member items listed without price.</summary>
|
/// <summary>Default: show the set as one priced line; member items listed without price.</summary>
|
||||||
SetPrice,
|
SetPrice,
|
||||||
/// <summary>Show each member item with its own price; the set line is a header without price.</summary>
|
|
||||||
ItemPrices,
|
|
||||||
/// <summary>Show only the set as one priced line; member items are removed entirely.</summary>
|
/// <summary>Show only the set as one priced line; member items are removed entirely.</summary>
|
||||||
SetOnly
|
SetOnly
|
||||||
}
|
}
|
||||||
@@ -32,6 +30,14 @@ public sealed class InvoiceSetLine
|
|||||||
public decimal TotalNet { get; init; }
|
public decimal TotalNet { get; init; }
|
||||||
/// <summary>When false, the price/total cells are rendered blank (e.g. set members in SetPrice mode).</summary>
|
/// <summary>When false, the price/total cells are rendered blank (e.g. set members in SetPrice mode).</summary>
|
||||||
public bool ShowPrice { get; init; } = true;
|
public bool ShowPrice { get; init; } = true;
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this line carries a position number. Mirrors the editor's numbering
|
||||||
|
/// (<c>InvoiceDraftCalculator.RecomputePositions</c>): every line is numbered <b>except</b>
|
||||||
|
/// free-text/heading lines — independent of whether a price is shown, so a price-blanked set
|
||||||
|
/// member (SetPrice mode / block-mode nulled member) is still numbered exactly like in the
|
||||||
|
/// online editor.
|
||||||
|
/// </summary>
|
||||||
|
public bool Numbered { get; init; } = true;
|
||||||
/// <summary>True for the set header line (rendered emphasised).</summary>
|
/// <summary>True for the set header line (rendered emphasised).</summary>
|
||||||
public bool IsSetHeader { get; init; }
|
public bool IsSetHeader { get; init; }
|
||||||
}
|
}
|
||||||
@@ -43,15 +49,22 @@ public sealed class InvoiceSetLine
|
|||||||
/// header's <c>id</c>. Items that belong to no set pass through unchanged.
|
/// header's <c>id</c>. Items that belong to no set pass through unchanged.
|
||||||
///
|
///
|
||||||
/// The invoice total is taken from the registration balance, not from these
|
/// The invoice total is taken from the registration balance, not from these
|
||||||
/// lines, so switching modes is purely presentational and never changes the
|
/// lines, so calling <see cref="Build"/> is a pure, non-mutating transformation
|
||||||
/// invoice sum — set price always equals the sum of its members.
|
/// of the items it is given and never changes the invoice sum. A set header
|
||||||
|
/// only adopts the chosen <see cref="SetDisplayMode"/> once it has actually been
|
||||||
|
/// converted (its own <c>total_net</c> is non-zero, via the one-way set-item
|
||||||
|
/// switch, <c>InvoiceDraftEditService.ApplyItemSetPrice</c>) — until then the
|
||||||
|
/// header renders blank and each member keeps its own individual price, exactly
|
||||||
|
/// like a non-set item. (The choice of which mode to apply is itself a
|
||||||
|
/// persisted, effectively one-way decision on the draft session — see
|
||||||
|
/// <c>Fuchs/Docs/INVOICE_SET_PRICING.md</c> — but that persistence lives outside
|
||||||
|
/// this class.)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class InvoiceSetPricing
|
public static class InvoiceSetPricing
|
||||||
{
|
{
|
||||||
public static SetDisplayMode ParseMode(string? raw) =>
|
public static SetDisplayMode ParseMode(string? raw) =>
|
||||||
(raw ?? "").Trim().ToLowerInvariant() switch
|
(raw ?? "").Trim().ToLowerInvariant() switch
|
||||||
{
|
{
|
||||||
"itemprices" or "items" or "item" => SetDisplayMode.ItemPrices,
|
|
||||||
"setonly" or "set_only" => SetDisplayMode.SetOnly,
|
"setonly" or "set_only" => SetDisplayMode.SetOnly,
|
||||||
_ => SetDisplayMode.SetPrice
|
_ => SetDisplayMode.SetPrice
|
||||||
};
|
};
|
||||||
@@ -71,9 +84,21 @@ public static class InvoiceSetPricing
|
|||||||
public static bool ContainsSets(IEnumerable<Dictionary<string, object?>> items) =>
|
public static bool ContainsSets(IEnumerable<Dictionary<string, object?>> items) =>
|
||||||
items.Any(IsSetHeader) || items.Any(i => !string.IsNullOrEmpty(SetIdOf(i)));
|
items.Any(IsSetHeader) || items.Any(i => !string.IsNullOrEmpty(SetIdOf(i)));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if the block carries a <c>SetItmId</c>-linked set (a member points at a header via
|
||||||
|
/// <c>setId</c>) — i.e. the function-1 "set-item switch" grouping. This is the only case that
|
||||||
|
/// still routes through <see cref="Build"/>; the two menu modes (ADR 0009) clear <c>setId</c>
|
||||||
|
/// and render flat (their inserted set row is emphasised on its own, members null out to blank).
|
||||||
|
/// </summary>
|
||||||
|
public static bool HasSetMembers(IEnumerable<Dictionary<string, object?>> items) =>
|
||||||
|
items.Any(i => !string.IsNullOrEmpty(SetIdOf(i)));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Produces the ordered display lines for the given items and mode.
|
/// Produces the ordered display lines for the given items and mode.
|
||||||
/// Standalone items are always shown with their price.
|
/// Standalone items are always shown with their price. A set header whose
|
||||||
|
/// own <c>total_net</c> is still zero (not yet converted via the set-item
|
||||||
|
/// switch) is rendered blank with its members individually priced,
|
||||||
|
/// regardless of <paramref name="mode"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static List<InvoiceSetLine> Build(IReadOnlyList<Dictionary<string, object?>> items, SetDisplayMode mode)
|
public static List<InvoiceSetLine> Build(IReadOnlyList<Dictionary<string, object?>> items, SetDisplayMode mode)
|
||||||
{
|
{
|
||||||
@@ -92,20 +117,27 @@ public static class InvoiceSetPricing
|
|||||||
{
|
{
|
||||||
string setId = HeaderIdOf(item);
|
string setId = HeaderIdOf(item);
|
||||||
var members = membersBySet.TryGetValue(setId, out var m) ? m : new List<Dictionary<string, object?>>();
|
var members = membersBySet.TryGetValue(setId, out var m) ? m : new List<Dictionary<string, object?>>();
|
||||||
decimal setTot = HeaderTotal(item, members);
|
FuchsPdf.ParseDec(item.no("total_net", 0), out decimal headerOwnTotal);
|
||||||
|
|
||||||
|
if (headerOwnTotal == 0)
|
||||||
|
{
|
||||||
|
// Not yet converted (the set-item switch, ApplyItemSetPrice, hasn't run): the
|
||||||
|
// header still carries its as-delivered zero price, so the chosen display mode
|
||||||
|
// does not apply yet — show the header blank and each member with its own,
|
||||||
|
// individual price, exactly as if there were no set grouping at all.
|
||||||
|
result.Add(HeaderLine(item, 0, showPrice: false));
|
||||||
|
foreach (var mem in members) result.Add(MemberLine(mem, showPrice: true));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
switch (mode)
|
switch (mode)
|
||||||
{
|
{
|
||||||
case SetDisplayMode.SetPrice:
|
case SetDisplayMode.SetPrice:
|
||||||
result.Add(HeaderLine(item, setTot, showPrice: true));
|
result.Add(HeaderLine(item, headerOwnTotal, showPrice: true));
|
||||||
foreach (var mem in members) result.Add(MemberLine(mem, showPrice: false));
|
foreach (var mem in members) result.Add(MemberLine(mem, showPrice: false));
|
||||||
break;
|
break;
|
||||||
case SetDisplayMode.ItemPrices:
|
|
||||||
result.Add(HeaderLine(item, setTot, showPrice: false)); // grouping title, no price (avoid double count)
|
|
||||||
foreach (var mem in members) result.Add(MemberLine(mem, showPrice: true));
|
|
||||||
break;
|
|
||||||
case SetDisplayMode.SetOnly:
|
case SetDisplayMode.SetOnly:
|
||||||
result.Add(HeaderLine(item, setTot, showPrice: true));
|
result.Add(HeaderLine(item, headerOwnTotal, showPrice: true));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,15 +180,6 @@ public static class InvoiceSetPricing
|
|||||||
return string.IsNullOrEmpty(s) ? i.nz("id", "") : s;
|
return string.IsNullOrEmpty(s) ? i.nz("id", "") : s;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static decimal HeaderTotal(Dictionary<string, object?> header, List<Dictionary<string, object?>> members)
|
|
||||||
{
|
|
||||||
FuchsPdf.ParseDec(header.no("total_net", 0), out decimal headerTot);
|
|
||||||
if (headerTot != 0) return headerTot;
|
|
||||||
decimal sum = 0;
|
|
||||||
foreach (var m in members) { FuchsPdf.ParseDec(m.no("total_net", 0), out decimal t); sum += t; }
|
|
||||||
return sum;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static InvoiceSetLine HeaderLine(Dictionary<string, object?> i, decimal setTotal, bool showPrice) => new()
|
private static InvoiceSetLine HeaderLine(Dictionary<string, object?> i, decimal setTotal, bool showPrice) => new()
|
||||||
{
|
{
|
||||||
Id = i.nz("id", ""),
|
Id = i.nz("id", ""),
|
||||||
@@ -166,6 +189,7 @@ public static class InvoiceSetPricing
|
|||||||
PriceNet = setTotal,
|
PriceNet = setTotal,
|
||||||
TotalNet = setTotal,
|
TotalNet = setTotal,
|
||||||
ShowPrice = showPrice,
|
ShowPrice = showPrice,
|
||||||
|
Numbered = true, // a set header is always numbered
|
||||||
IsSetHeader = true
|
IsSetHeader = true
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -182,6 +206,7 @@ public static class InvoiceSetPricing
|
|||||||
PriceNet = price,
|
PriceNet = price,
|
||||||
TotalNet = total,
|
TotalNet = total,
|
||||||
ShowPrice = showPrice && !IsNoPriceLine(i), // headings/free text print no price
|
ShowPrice = showPrice && !IsNoPriceLine(i), // headings/free text print no price
|
||||||
|
Numbered = !IsNoPriceLine(i), // …but are still numbered unless heading/free-text
|
||||||
IsSetHeader = false
|
IsSetHeader = false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"dotnet-ef": {
|
||||||
|
"version": "10.0.10",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-ef"
|
||||||
|
],
|
||||||
|
"rollForward": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -130,10 +130,13 @@ $inv.d = {
|
|||||||
return h;
|
return h;
|
||||||
},
|
},
|
||||||
/* Seed the authoritative server session from the assembled editor payload. Called
|
/* Seed the authoritative server session from the assembled editor payload. Called
|
||||||
once from invSumUpdate on open; thereafter the backend is the source of truth. */
|
once from invSumUpdate on open; thereafter the backend is the source of truth.
|
||||||
|
The in-flight ajax promise is stashed on 'dseedpromise' so a delta sent (e.g. via
|
||||||
|
a context-menu action) before seeding has actually completed can wait for the token
|
||||||
|
instead of silently no-op'ing (see $inv.d.sync). */
|
||||||
seed: function (payload) {
|
seed: function (payload) {
|
||||||
let l = $inv.d.layout(); l.aC('freeze');
|
let l = $inv.d.layout(); l.aC('freeze');
|
||||||
$ocms.postXT({
|
let p = $ocms.postXT({
|
||||||
url: $ocms.url('inv/dopen'), data: { payload: JSON.stringify(payload) }, success: (r) => {
|
url: $ocms.url('inv/dopen'), data: { payload: JSON.stringify(payload) }, success: (r) => {
|
||||||
$inv.d.tbl().data('dtoken', r.token).data('dver', r.version).data('dhashes', $inv.d.hashes()).data('dorder', $inv.d.order());
|
$inv.d.tbl().data('dtoken', r.token).data('dver', r.version).data('dhashes', $inv.d.hashes()).data('dorder', $inv.d.order());
|
||||||
$fis.draft.bind(r.token, {
|
$fis.draft.bind(r.token, {
|
||||||
@@ -142,8 +145,10 @@ $inv.d = {
|
|||||||
onClosed: (reason) => $inv.d.closed(reason)
|
onClosed: (reason) => $inv.d.closed(reason)
|
||||||
});
|
});
|
||||||
$inv.d.refresh();
|
$inv.d.refresh();
|
||||||
}, error: () => { l.rC('freeze'); }, complete: () => { $inv.d.tbl().removeData('dseeding'); }
|
}, error: () => { l.rC('freeze'); }, complete: () => { $inv.d.tbl().removeData('dseeding').removeData('dseedpromise'); }
|
||||||
});
|
});
|
||||||
|
$inv.d.tbl().data('dseedpromise', p);
|
||||||
|
return p;
|
||||||
},
|
},
|
||||||
/* Re-fetch the authoritative state and render the totals footer + validation from it. */
|
/* Re-fetch the authoritative state and render the totals footer + validation from it. */
|
||||||
refresh: function (cb) {
|
refresh: function (cb) {
|
||||||
@@ -157,11 +162,21 @@ $inv.d = {
|
|||||||
},
|
},
|
||||||
applyState: function (state) {
|
applyState: function (state) {
|
||||||
let tbl = $inv.d.tbl(); if (tbl.length < 1) { return; }
|
let tbl = $inv.d.tbl(); if (tbl.length < 1) { return; }
|
||||||
|
/* Concurrent refreshes (one per flushed block plus one for a follow-up delta such as
|
||||||
|
item.setprice, and SignalR draftReady on top) can have their dstate responses arrive
|
||||||
|
out of order. Applying a response older than the version already rendered would
|
||||||
|
silently roll back a just-applied server-side mutation (e.g. a set-price conversion
|
||||||
|
reverting to its pre-conversion 0/individually-priced state) — so stale responses are
|
||||||
|
dropped here instead of rendered. */
|
||||||
|
let curVer = tbl.data('dver');
|
||||||
|
if (typeof curVer === 'number' && typeof state.version === 'number' && state.version < curVer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
tbl.data('dver', state.version).data('serverSums', state.sums);
|
tbl.data('dver', state.version).data('serverSums', state.sums);
|
||||||
$inv.d.footer(tbl, state.sums || {}, state.admin || {});
|
$inv.d.footer(tbl, state.sums || {}, state.admin || {});
|
||||||
$inv.d.notes(state.notes || []);
|
$inv.d.notes(state.notes || []);
|
||||||
$inv.d.validation(state.validation || []);
|
$inv.d.validation(state.validation || []);
|
||||||
$inv.d.applyPositions(tbl, state.req || []);
|
$inv.d.applyItems(tbl, state.req || []);
|
||||||
$inv.d.applySetDisplay(tbl, state.setDisplay || {});
|
$inv.d.applySetDisplay(tbl, state.setDisplay || {});
|
||||||
/* Keep the menu's "Set-Preisanzeige" entry in sync with the authoritative admin.setmode:
|
/* Keep the menu's "Set-Preisanzeige" entry in sync with the authoritative admin.setmode:
|
||||||
hides it right after a mode switch, and restores it if a discard reverted the draft to
|
hides it right after a mode switch, and restores it if a discard reverted the draft to
|
||||||
@@ -181,19 +196,60 @@ $inv.d = {
|
|||||||
/* Push the server's authoritative position numbers back onto the rendered rows so the online
|
/* Push the server's authoritative position numbers back onto the rendered rows so the online
|
||||||
editor and the PDF preview always agree (the server numbers priced lines continuously; the
|
editor and the PDF preview always agree (the server numbers priced lines continuously; the
|
||||||
browser must not keep its own numbering). Only the position cell is touched — no re-render. */
|
browser must not keep its own numbering). Only the position cell is touched — no re-render. */
|
||||||
applyPositions: function (tbl, req) {
|
/* Reconciles each block's rendered rows against the authoritative server line list
|
||||||
(req || []).forEach((b) => (b && b.itm || []).forEach((co) => {
|
(`req[].itm`, the co shape). The server may not only change a line's values but also
|
||||||
if (!co || (co.id || '') === '') { return; }
|
ADD lines (the block set row inserted by "Set mit Preis"/block.setprice) and REMOVE lines
|
||||||
let cell = tbl.find('#itm' + co.id + ' td.keep').first();
|
("Nur Set mit Preis"/block.setonly), and reorder them — the browser never computes any of
|
||||||
if (cell.length) { cell.text(co.p != null ? co.p : ''); }
|
that (ADR 0006/0008/0009), it just mirrors the server. For each server line: patch/insert
|
||||||
}));
|
the matching row, preserving null price fields verbatim (ADR 0009: null = empty cell, kept
|
||||||
|
distinct from 0). Rows the server no longer lists are removed; order follows the server.
|
||||||
|
Position numbers, net/VAT and the set-row emphasis all come from rrw off the mirrored data. */
|
||||||
|
applyItems: function (tbl, req) {
|
||||||
|
let val = (x) => (x === undefined ? null : x); // JSON null stays null; absent -> null
|
||||||
|
(req || []).forEach((b) => {
|
||||||
|
if (!b) { return; }
|
||||||
|
let bid = (b.Id || '').toString(); if (bid === '') { return; }
|
||||||
|
let bdy = tbl.children('tbody').filter((i, e) => (($(e).data() || {}).Id || '').toString() === bid).first();
|
||||||
|
if (bdy.length < 1) { return; }
|
||||||
|
let itms = b.itm || [], serverIds = {};
|
||||||
|
let title = bdy.find('tr.title').first();
|
||||||
|
let prev = title.length ? title : null;
|
||||||
|
itms.forEach((co) => {
|
||||||
|
let id = (co.id || '').toString(); if (id === '') { return; }
|
||||||
|
serverIds[id] = true;
|
||||||
|
let rw = bdy.find('#itm' + id).first(), isNew = rw.length < 1;
|
||||||
|
if (isNew) { rw = $$.tr(bdy, { id: 'itm' + id, class: 'itm' }); }
|
||||||
|
let dta = rw.data() || {};
|
||||||
|
let changed = isNew || (dta.Type || '') !== (co.typ || '')
|
||||||
|
|| dta.net !== val(co.v) || dta.net_val !== val(co.vt) || dta.vat_val !== val(co.vv)
|
||||||
|
|| dta.svcnet_val !== val(co.vs) || dta.svcvat_val !== val(co.vsv)
|
||||||
|
|| (dta.position || '') !== (co.p != null ? co.p : '');
|
||||||
|
$.extend(dta, {
|
||||||
|
Id: id, Type: co.typ,
|
||||||
|
net: val(co.v), net_val: val(co.vt), vat_val: val(co.vv),
|
||||||
|
svcnet_val: val(co.vs), svcvat_val: val(co.vsv),
|
||||||
|
vat: co.vat, position: (co.p != null ? co.p : ''),
|
||||||
|
SetItmId: (co.SetItmId != null ? co.SetItmId : null)
|
||||||
|
});
|
||||||
|
if (isNew) { $.extend(dta, { htmltext: co.t, quantity: co.q }); } // text only for server-created rows (e.g. the set row)
|
||||||
|
rw.data(dta);
|
||||||
|
if (prev) { if (rw.prev()[0] !== prev[0]) { rw.insertAfter(prev); } } // keep server order, but don't churn already-ordered rows
|
||||||
|
else if (bdy.children().first()[0] !== rw[0]) { bdy.prepend(rw); }
|
||||||
|
prev = rw;
|
||||||
|
if (changed) { $inv.rrw.call(rw); }
|
||||||
|
});
|
||||||
|
bdy.find('tr.itm').each((i, tr) => {
|
||||||
|
let id = (($(tr).data() || {}).Id || '').toString();
|
||||||
|
if (id !== '' && !serverIds[id]) { $(tr).remove(); } // dropped server-side (block.setonly)
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
/* Applies the backend-authoritative set-pricing display flags (see BuildSetDisplay /
|
/* Applies the backend-authoritative set-pricing display flags (see BuildSetDisplay /
|
||||||
InvoiceSetPricing) onto the rendered rows: blanks the price/total cells for lines the
|
InvoiceSetPricing) onto the rendered rows: blanks the price/total cells for lines the
|
||||||
server says should show no price (set members in SetPrice mode, the set header in
|
server says should show no price (set members in SetPrice/SetOnly mode), and emphasises
|
||||||
ItemPrices mode), and emphasises the set header line — so the online editor always
|
the set header line — so the online editor always shows exactly what the PDF will print,
|
||||||
shows exactly what the PDF will print, without duplicating the pricing rules client-side.
|
without duplicating the pricing rules client-side. Rows not present in the map (no set in
|
||||||
Rows not present in the map (no set in that block, or standalone items) are left untouched. */
|
that block, or standalone items) are left untouched. */
|
||||||
applySetDisplay: function (tbl, setDisplay) {
|
applySetDisplay: function (tbl, setDisplay) {
|
||||||
tbl.find('tr.itm').each((i, tr) => {
|
tbl.find('tr.itm').each((i, tr) => {
|
||||||
let rw = $(tr), dta = rw.data() || {}, id = (dta.Id || '').toString();
|
let rw = $(tr), dta = rw.data() || {}, id = (dta.Id || '').toString();
|
||||||
@@ -205,11 +261,23 @@ $inv.d = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
/* Send one change to the server; the draftReady signal and this success both refresh. */
|
/* Send one change to the server; the draftReady signal and this success both refresh.
|
||||||
|
Returns the underlying ajax promise so callers that must not race a follow-up delta
|
||||||
|
against this one (e.g. $inv.toSetPrice after a flush) can chain on it.
|
||||||
|
If the initial seeding (inv/dopen) is still in flight (no token yet, but a
|
||||||
|
'dseedpromise' is pending), the delta previously vanished silently — the user saw no
|
||||||
|
network activity at all when clicking a context-menu action right after opening the
|
||||||
|
editor. Now the delta waits for the seed to finish and retries against the freshly
|
||||||
|
issued token instead of being dropped. */
|
||||||
sync: function (delta) {
|
sync: function (delta) {
|
||||||
let t = $inv.d.token(); if (t === '') { return; }
|
let t = $inv.d.token();
|
||||||
|
if (t === '') {
|
||||||
|
let seeding = $inv.d.tbl().data('dseedpromise');
|
||||||
|
if (seeding) { return $.when(seeding).then(() => $inv.d.sync(delta)); }
|
||||||
|
return $.when();
|
||||||
|
}
|
||||||
$inv.d.layout().aC('freeze');
|
$inv.d.layout().aC('freeze');
|
||||||
$ocms.postXT({
|
return $ocms.postXT({
|
||||||
url: $ocms.url('inv/dpatch'), data: { token: t, delta: JSON.stringify(delta) },
|
url: $ocms.url('inv/dpatch'), data: { token: t, delta: JSON.stringify(delta) },
|
||||||
success: () => { $inv.d.refresh(); },
|
success: () => { $inv.d.refresh(); },
|
||||||
error: (xhr) => { $inv.d.layout().rC('freeze'); if (xhr && xhr.status === 410) { $inv.d.closed('expired'); } }
|
error: (xhr) => { $inv.d.layout().rC('freeze'); if (xhr && xhr.status === 410) { $inv.d.closed('expired'); } }
|
||||||
@@ -220,22 +288,29 @@ $inv.d = {
|
|||||||
/* Diff the freshly-rebuilt blocks against the last-synced state and send only the
|
/* Diff the freshly-rebuilt blocks against the last-synced state and send only the
|
||||||
changed/removed blocks as granular block.replace / block.remove deltas. A pure section
|
changed/removed blocks as granular block.replace / block.remove deltas. A pure section
|
||||||
reorder (same blocks, new sequence) changes no block hash, so it is sent separately as a
|
reorder (same blocks, new sequence) changes no block hash, so it is sent separately as a
|
||||||
block.order delta; the server reorders the cache, renumbers positions and pushes them back. */
|
block.order delta; the server reorders the cache, renumbers positions and pushes them back.
|
||||||
|
No early return on an empty token here: sync() itself waits for an in-flight seed
|
||||||
|
(dseedpromise) instead of silently dropping the delta, so edits made in the brief window
|
||||||
|
before seeding completes are not lost. */
|
||||||
syncChanged: function (tbl) {
|
syncChanged: function (tbl) {
|
||||||
if (($inv.d.token()) === '') { return; }
|
let bai = tbl.data('bai') || [], prev = tbl.data('dhashes') || {}, next = {}, changed = [], removed = [], pending = [];
|
||||||
let bai = tbl.data('bai') || [], prev = tbl.data('dhashes') || {}, next = {}, changed = [], removed = [];
|
|
||||||
$.each(bai, (i, b) => { let id = (b.Id || '').toString(), h = JSON.stringify(b); next[id] = h; if (prev[id] !== h) { changed.push(b); } });
|
$.each(bai, (i, b) => { let id = (b.Id || '').toString(), h = JSON.stringify(b); next[id] = h; if (prev[id] !== h) { changed.push(b); } });
|
||||||
$.each(prev, (id) => { if (next[id] === undefined) { removed.push(id); } });
|
$.each(prev, (id) => { if (next[id] === undefined) { removed.push(id); } });
|
||||||
let order = $inv.d.order(), prevOrder = tbl.data('dorder') || [];
|
let order = $inv.d.order(), prevOrder = tbl.data('dorder') || [];
|
||||||
tbl.data('dhashes', next).data('dorder', order);
|
tbl.data('dhashes', next).data('dorder', order);
|
||||||
changed.forEach((b) => $inv.d.sync({ Target: 'block.replace', Ref: (b.Id || '').toString(), Value: b }));
|
changed.forEach((b) => pending.push($inv.d.sync({ Target: 'block.replace', Ref: (b.Id || '').toString(), Value: b })));
|
||||||
removed.forEach((id) => $inv.d.sync({ Target: 'block.remove', Ref: id }));
|
removed.forEach((id) => pending.push($inv.d.sync({ Target: 'block.remove', Ref: id })));
|
||||||
let sameSet = prevOrder.length === order.length && prevOrder.slice().sort().join(',') === order.slice().sort().join(',');
|
let sameSet = prevOrder.length === order.length && prevOrder.slice().sort().join(',') === order.slice().sort().join(',');
|
||||||
if (sameSet && prevOrder.join(',') !== order.join(',')) { $inv.d.sync({ Target: 'block.order', Value: order }); }
|
if (sameSet && prevOrder.join(',') !== order.join(',')) { pending.push($inv.d.sync({ Target: 'block.order', Value: order })); }
|
||||||
|
/* $.when(...pending) so a caller (e.g. toSetPrice) can wait until every flushed block
|
||||||
|
has actually reached the server before sending a further delta that depends on it —
|
||||||
|
otherwise a delayed block.replace carrying pre-conversion values can land after and
|
||||||
|
silently overwrite a just-applied server-side mutation (e.g. item.setprice). */
|
||||||
|
return $.when.apply($, pending).promise();
|
||||||
},
|
},
|
||||||
/* Map an inline recipient field to its delta target and send it. */
|
/* Map an inline recipient field to its delta target and send it. No token-guard here for the
|
||||||
|
same reason as syncChanged: sync() waits for an in-flight seed instead of dropping the edit. */
|
||||||
syncField: function (nme, val) {
|
syncField: function (nme, val) {
|
||||||
if ($inv.d.token() === '') { return; }
|
|
||||||
let map = { invoicetitle: 'title', invoiceaddress: 'address', invoiceemail: 'email', loc: 'provisionlocation', provisionlocation: 'provisionlocation', provisionperiod: 'provisionperiod' };
|
let map = { invoicetitle: 'title', invoiceaddress: 'address', invoiceemail: 'email', loc: 'provisionlocation', provisionlocation: 'provisionlocation', provisionperiod: 'provisionperiod' };
|
||||||
let target = map[nme]; if (!target) { return; }
|
let target = map[nme]; if (!target) { return; }
|
||||||
$inv.d.sync({ Target: target, Value: val });
|
$inv.d.sync({ Target: target, Value: val });
|
||||||
@@ -390,6 +465,12 @@ $inv.rd = {
|
|||||||
},
|
},
|
||||||
applyState: function (state) {
|
applyState: function (state) {
|
||||||
let tbl = $inv.rd.tbl(); if (tbl.length < 1) { return; }
|
let tbl = $inv.rd.tbl(); if (tbl.length < 1) { return; }
|
||||||
|
/* See $inv.d.applyState — drop stale/out-of-order dstate responses so an older refresh
|
||||||
|
can never roll back a newer one's totals/validation. */
|
||||||
|
let curVer = tbl.data('rdver');
|
||||||
|
if (typeof curVer === 'number' && typeof state.version === 'number' && state.version < curVer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
tbl.data('rdver', state.version).data('serverSums', state.sums).data('remid', state.remid || '');
|
tbl.data('rdver', state.version).data('serverSums', state.sums).data('remid', state.remid || '');
|
||||||
$inv.rd.footer(tbl, state.sums || {});
|
$inv.rd.footer(tbl, state.sums || {});
|
||||||
$inv.rd.notes(state.notes || {});
|
$inv.rd.notes(state.notes || {});
|
||||||
@@ -949,6 +1030,8 @@ $inv.eHtml = function (ev) {
|
|||||||
}
|
}
|
||||||
$ocms.dlgform(flds, sets);
|
$ocms.dlgform(flds, sets);
|
||||||
};
|
};
|
||||||
|
/* No client-side arithmetic (ADR 0006/0008): only the chosen VAT rate is stored on each row;
|
||||||
|
the server (InvoiceDraftCalculator.RecomputeLineValues) recomputes vat_val/svcvat_val from it. */
|
||||||
$inv.setVat = function (ev) {
|
$inv.setVat = function (ev) {
|
||||||
let t = $(this), thisrow = ev.data;
|
let t = $(this), thisrow = ev.data;
|
||||||
let vat = prompt($rct.rqV);
|
let vat = prompt($rct.rqV);
|
||||||
@@ -961,12 +1044,6 @@ $inv.setVat = function (ev) {
|
|||||||
thisrow.siblings('.itm').each(function () {
|
thisrow.siblings('.itm').each(function () {
|
||||||
let rwi = $(this), dta = rwi.data();
|
let rwi = $(this), dta = rwi.data();
|
||||||
dta.vat = fnum(vat, { style: 'percent' }).replace(' ', '');
|
dta.vat = fnum(vat, { style: 'percent' }).replace(' ', '');
|
||||||
if ((dta.net_val || 0) > 0) {
|
|
||||||
dta.vat_val = dta.net_val * vat;
|
|
||||||
}
|
|
||||||
if ((dta.svcnet_val || 0) > 0) {
|
|
||||||
dta.svcvat_val = dta.svcnet_val * vat;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
$inv.t_fds_inv();
|
$inv.t_fds_inv();
|
||||||
}
|
}
|
||||||
@@ -1026,12 +1103,14 @@ $inv.rrw = function () {
|
|||||||
if (ph === false) {
|
if (ph === false) {
|
||||||
bc.unshift($$.dc('ibtn edit', { title: $rct.cP }).append(gi('pencil')).click(rw, $inv.eRow));
|
bc.unshift($$.dc('ibtn edit', { title: $rct.cP }).append(gi('pencil')).click(rw, $inv.eRow));
|
||||||
bc.push($$.dc('ibtn del', { title: $rct.dR }).append(gi('trash')).click(function (e) { if (confirm($rct.cD)) { rw.remove(); $inv.t_fds_inv(); } }));
|
bc.push($$.dc('ibtn del', { title: $rct.dR }).append(gi('trash')).click(function (e) { if (confirm($rct.cD)) { rw.remove(); $inv.t_fds_inv(); } }));
|
||||||
/* "Auf Setpreis umstellen": only offered on a Set header item that is not yet
|
/* "Auf Setpreis umstellen": offered on a Set header item that does not yet carry a
|
||||||
priced from its members (fds__prepInvoice's [SetItmId] is null exactly for a
|
price. fds__prepInvoice's [SetItmId] now anchors on the still-unconverted (price 0)
|
||||||
Set header — either already set-priced, or not yet converted). Irreversible:
|
header, so the header row's own SetItmId self-references its own id rather than being
|
||||||
sets this row's price to the sum of its members (rows whose [SetItmId] equals
|
null — the real signal for showing this button is still the header's own price/net_val
|
||||||
this row's id) and clears each member's price. */
|
being 0 (once converted it carries a nonzero price and the button disappears).
|
||||||
if ((dta.Type || '').toLowerCase() === 'set' && (dta.SetItmId || '') === '') {
|
Irreversible: the actual sum/clear mutation happens server-side (backend-authoritative,
|
||||||
|
see InvoiceDraftEditService.ApplyItemSetPrice) via the "item.setprice" patch target. */
|
||||||
|
if ((dta.Type || '').toLowerCase() === 'set' && (dta.net_val || 0) === 0) {
|
||||||
bc.push($$.dc('ibtn tosetp', { title: $rct.toSetP }).append(gi('scale')).click(rw, $inv.toSetPrice));
|
bc.push($$.dc('ibtn tosetp', { title: $rct.toSetP }).append(gi('scale')).click(rw, $inv.toSetPrice));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1041,7 +1120,7 @@ $inv.rrw = function () {
|
|||||||
} else if (rw.is('.itm.osum') === true) {
|
} else if (rw.is('.itm.osum') === true) {
|
||||||
co = { invrqid: dta.InvRqId, id: 'osum' + rw.index(), typ: 'osum', p: '', q: null, t: oHtml(dta.tbl.tbl), tt: null, v: null, vt: dta.net_val, vs: dta.svcnet_val, vat: dta.vat, vv: dta.vat_val, vsv: dta.svcvat_val, det: false };
|
co = { invrqid: dta.InvRqId, id: 'osum' + rw.index(), typ: 'osum', p: '', q: null, t: oHtml(dta.tbl.tbl), tt: null, v: null, vt: dta.net_val, vs: dta.svcnet_val, vat: dta.vat, vv: dta.vat_val, vsv: dta.svcvat_val, det: false };
|
||||||
} else {
|
} else {
|
||||||
co = { invrqid: dta.InvRqId, id: dta.Id || '', typ: dta.Type || 'other', p: '', q: null, t: '', tt: null, v: null, vt: dta.net_val, vs: dta.svcnet_val, vat: dta.vat, vv: dta.vat_val, vsv: dta.svcvat_val, det: (dta.Note || '') !== '' && hn === false };
|
co = { invrqid: dta.InvRqId, id: dta.Id || '', typ: dta.Type || 'other', p: '', q: null, t: '', tt: null, v: null, vt: dta.net_val, vs: dta.svcnet_val, vat: dta.vat, vv: dta.vat_val, vsv: dta.svcvat_val, det: (dta.Note || '') !== '' && hn === false, SetItmId: dta.SetItmId || null };
|
||||||
$$.dc('ibtn ico move', axf, { title: $rct.mR }); /* should be the last added */
|
$$.dc('ibtn ico move', axf, { title: $rct.mR }); /* should be the last added */
|
||||||
|
|
||||||
co.p = dta.position || (dta.SortOrder || '');
|
co.p = dta.position || (dta.SortOrder || '');
|
||||||
@@ -1051,7 +1130,7 @@ $inv.rrw = function () {
|
|||||||
co.t = dta.htmltext || (((dta.NameOrNumber || '').substr(0, 1) !== '#' ? oHtml($$[0]('p').text(dta.NameOrNumber)) : '') + (dta.Note || ''));
|
co.t = dta.htmltext || (((dta.NameOrNumber || '').substr(0, 1) !== '#' ? oHtml($$[0]('p').text(dta.NameOrNumber)) : '') + (dta.Note || ''));
|
||||||
} else {
|
} else {
|
||||||
co.tt = co.det ? '' : $$.s(dta.Note || '').text();
|
co.tt = co.det ? '' : $$.s(dta.Note || '').text();
|
||||||
co.q = dta.quantity || (fnum(dta.quantityhours) + ' ' + (dta.UnitString || ''));
|
co.q = dta.quantity || ((dta.quantityhours || 0) !== 0 ? (fnum(dta.quantityhours) + ' ' + (dta.UnitString || '')) : ''); /* guard: no quantityhours -> blank, not fnum(undefined)="NaN" (e.g. the block set row) */
|
||||||
co.t = dta.htmltext ||(co.det ? (oHtml($$.s(dta.NameOrNumber || '')) + oHtml($$.dc('desc').html(dta.Note))) : oHtml($$.s(dta.NameOrNumber || '')));
|
co.t = dta.htmltext ||(co.det ? (oHtml($$.s(dta.NameOrNumber || '')) + oHtml($$.dc('desc').html(dta.Note))) : oHtml($$.s(dta.NameOrNumber || '')));
|
||||||
co.v = dta.net
|
co.v = dta.net
|
||||||
co.vt = dta.net_val
|
co.vt = dta.net_val
|
||||||
@@ -1070,75 +1149,62 @@ $inv.rrw = function () {
|
|||||||
tda.push($$.td(rw, { colspan: 4 }).append(co.t));
|
tda.push($$.td(rw, { colspan: 4 }).append(co.t));
|
||||||
} else {
|
} else {
|
||||||
Array.prototype.push.apply(tda, co.q ? [$$.tdc('keep').text(co.q)] : []);
|
Array.prototype.push.apply(tda, co.q ? [$$.tdc('keep').text(co.q)] : []);
|
||||||
|
/* ADR 0009: a null/undefined price renders an EMPTY cell (block-mode "Set mit Preis"
|
||||||
|
members, and the set row's own unit-price), distinct from a real 0,00 €. */
|
||||||
|
let cur = (x) => (x == null ? '' : fnum(x, $rct.cst));
|
||||||
Array.prototype.push.apply(tda, [
|
Array.prototype.push.apply(tda, [
|
||||||
$$.tdc('txt', { colspan: !co.q ? 2 : 1, title: co.tt }).append(co.t),
|
$$.tdc('txt', { colspan: !co.q ? 2 : 1, title: co.tt }).append(co.t),
|
||||||
$$.tdc('currency').text(fnum(co.v, $rct.cst)),
|
$$.tdc('currency').text(cur(co.v)),
|
||||||
$$.tdc('currency inetval').text(fnum(co.vt, $rct.cst)).attr('title', $rct.svcPart + ': ' + fnum(co.vs, $rct.cst))
|
$$.tdc('currency inetval').text(cur(co.vt)).attr('title', $rct.svcPart + ': ' + cur(co.vs))
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
rw.empty().attr('class', ph ? 'placeholder' : 'itm').aC(co.Typ).tC('hidenote', hn).append(tda);
|
rw.empty().attr('class', ph ? 'placeholder' : 'itm').aC(co.Typ)
|
||||||
|
.tC('sethdr', (co.typ || '').toString().toLowerCase() === 'set') /* emphasise set rows (fn1 header + block set row) */
|
||||||
|
.tC('hidenote', hn).append(tda);
|
||||||
dta.co = co;
|
dta.co = co;
|
||||||
};
|
};
|
||||||
/* "Auf Setpreis umstellen" (see $inv.rrw): sets this Set-header row's price to the sum of
|
/* "Auf Setpreis umstellen" (see $inv.rrw): backend-authoritative (ADR 0006) — the browser only
|
||||||
its member rows' totals (rows in the same block whose [SetItmId] equals this row's id,
|
posts the target set-header item id; the server (InvoiceDraftEditService.ApplyItemSetPrice)
|
||||||
the authoritative grouping from fds__prepInvoice), then clears each member's price so the
|
sums the member rows (same block, [SetItmId] equal to this row's id), writes that sum onto
|
||||||
member items show without a price — matching the SetPrice presentation, but as an actual,
|
the set header and clears each member's price, then the normal draftReady refresh re-renders
|
||||||
irreversible data change instead of a display-mode toggle (INVOICE_SET_PRICING.md's
|
both the header and its members from the authoritative session state. This mirrors the PDF's
|
||||||
setmode remains display-only and is unaffected by this). */
|
read of the same cached session, so the online editor and PDF can never drift apart. */
|
||||||
$inv.toSetPrice = function (ev) {
|
$inv.toSetPrice = function (ev) {
|
||||||
let rw = ev.data, dta = rw.data(), id = (dta.Id || '').toString();
|
let rw = ev.data, dta = rw.data(), id = (dta.Id || '').toString();
|
||||||
if (id === '') { return; }
|
if (id === '') { return; }
|
||||||
if (confirm($rct.toSetPc) === false) { return; }
|
if (confirm($rct.toSetPc) === false) { return; }
|
||||||
let bdy = rw.closest('tbody'), members = bdy.find('tr.itm').filter(function () {
|
/* Wait for the flush of any pending local edits to actually reach the server before sending
|
||||||
return (($(this).data('SetItmId') || '').toString()) === id;
|
the conversion delta — sending both in parallel let a delayed block.replace (still carrying
|
||||||
});
|
the pre-conversion, zero member/header values) land after item.setprice and silently
|
||||||
let sum_net_val = 0, sum_vat_val = 0, sum_svcnet_val = 0, sum_svcvat_val = 0;
|
overwrite the just-converted totals back to 0. */
|
||||||
members.each(function () {
|
$.when($inv.t_fds_inv()).done(() => { $inv.d.sync({ Target: 'item.setprice', Ref: id }); });
|
||||||
let m = $(this).data();
|
|
||||||
sum_net_val += (m.net_val || 0);
|
|
||||||
sum_vat_val += (m.vat_val || 0);
|
|
||||||
sum_svcnet_val += (m.svcnet_val || 0);
|
|
||||||
sum_svcvat_val += (m.svcvat_val || 0);
|
|
||||||
$.extend(m, { net: 0, net_val: 0, vat_val: 0, svcnet_val: 0, svcvat_val: 0, Discount: 0 });
|
|
||||||
$inv.rrw.call($(this));
|
|
||||||
});
|
|
||||||
$.extend(dta, {
|
|
||||||
net: sum_net_val, quantityhours: 1, Discount: 0,
|
|
||||||
net_val: sum_net_val, vat_val: sum_vat_val, svcnet_val: sum_svcnet_val, svcvat_val: sum_svcvat_val
|
|
||||||
});
|
|
||||||
$inv.rrw.call(rw);
|
|
||||||
$inv.t_fds_inv();
|
|
||||||
};
|
};
|
||||||
|
/* No client-side totals/footer/tax computation (ADR 0006/0008): this only re-assembles each
|
||||||
|
block's row contract array (needed to post the "req" shape to the server) and, on the very
|
||||||
|
first pass, seeds the authoritative backend session. Footer, VAT breakdown, service-refund
|
||||||
|
note figures and the per-block sum cell are never computed here — they are rendered exclusively
|
||||||
|
from the server's `dstate` response by $inv.d.footer / $inv.d.applyState once the draft has a
|
||||||
|
token. Before a token exists the footer/notes/isum cells are simply left empty; there is no
|
||||||
|
local approximation to display. */
|
||||||
$inv.invSumUpdate = function () {
|
$inv.invSumUpdate = function () {
|
||||||
let tbl = $(this), ft = tbl.children('tfoot').empty(), p13b = bool((tbl.data().admin || {}).p13b || '', false);
|
let tbl = $(this);
|
||||||
tbl.nextAll('.fnote').remove();
|
let ba = [];
|
||||||
let sms = { ttn: 0, ttb: 0, ttvat: 0, tscn: 0, tscvat: 0, vat: {}, itmnet: {} }, ba = [];
|
|
||||||
let rwcy = (lbl, val, cls) => $$.tdc('currency', $$.tr(ft, { class: cls || 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text(lbl)]), fnum(val, $rct.cst)), fn = (t) => $$.dc('fnote').insertAfter(tbl).rwText(t);
|
|
||||||
let csms = function (rrx, sms, sid) {
|
|
||||||
sms.tscn += (rrx.svcnet_val || 0);
|
|
||||||
sms.tscvat += (rrx.svcvat_val || 0);
|
|
||||||
sms.ttn += (rrx.net_val || 0);
|
|
||||||
sms.ttvat += (rrx.vat_val || 0);
|
|
||||||
sms.ttb += ((rrx.net_val || 0) + (rrx.vat_val || 0));
|
|
||||||
if ((rrx.vat || '') !== '') {
|
|
||||||
sms.vat[rrx.vat] = (sms.vat[rrx.vat] || 0) + (rrx.vat_val || 0);
|
|
||||||
}
|
|
||||||
//sms.itmnet[sid] = (sms.itmnet[sid] || 0) + (rrx.net_val || 0);
|
|
||||||
};
|
|
||||||
let bds = tbl.children('tbody');
|
let bds = tbl.children('tbody');
|
||||||
bds.each((bi, bdy) => {
|
bds.each((bi, bdy) => {
|
||||||
let b = $(bdy), rx = b.data() || {}, i = [], citems = [], cset = null, bnet = 0, itm = b.find('tr.itm'), iso = 0, ipos = 0;
|
let b = $(bdy), rx = b.data() || {}, i = [], citems = [], itm = b.find('tr.itm'), iso = 0, ipos = 0;
|
||||||
b.tC('empty', itm.length < 1);
|
b.tC('empty', itm.length < 1);
|
||||||
itm.each((ti, tx) => {
|
itm.each((ti, tx) => {
|
||||||
|
let rrx = $(tx).data() || {};
|
||||||
let rrx = $(tx).data() || {}; csms(rrx, sms, rx.Id); bnet += (rrx.net_val || 0); i.push(rrx.co);
|
i.push(rrx.co);
|
||||||
//console.debug('rrx %o', rrx);
|
|
||||||
/* backend item contract (title/desc/qty/price_net/total_net + set flags), see InvoiceSetPricing.
|
/* backend item contract (title/desc/qty/price_net/total_net + set flags), see InvoiceSetPricing.
|
||||||
Set grouping: an item of Type 'set' is a header that claims the following items in this
|
Set grouping: fds__prepInvoice computes [SetItmId] per item, anchored on the still-unconverted
|
||||||
block as its members until the next set header (mfr__items has no explicit member link). */
|
(price 0) Set header that owns it — this is the authoritative membership signal (not row order),
|
||||||
|
so items not tagged with a SetItmId by the server are never swept into a preceding set. The
|
||||||
|
header row's own SetItmId self-references its own id, but it is never its own member (excluded
|
||||||
|
by the id !== '' && !== citem.id check below). */
|
||||||
let citem = $inv.itemToContract(rrx);
|
let citem = $inv.itemToContract(rrx);
|
||||||
if (citem.type === 'set' && citem.id !== '') { cset = citem.id; }
|
let sid = (rrx.SetItmId || '').toString();
|
||||||
else if (cset !== null && (citem.id || '') !== '') { citem.setId = cset; }
|
if (citem.type !== 'set' && sid !== '' && sid !== citem.id) { citem.setId = sid; }
|
||||||
citems.push(citem);
|
citems.push(citem);
|
||||||
if (((typeof rrx.SortOrder === 'undefined' || rrx.SortOrder === null) ? -1 : rrx.SortOrder) > -1) {
|
if (((typeof rrx.SortOrder === 'undefined' || rrx.SortOrder === null) ? -1 : rrx.SortOrder) > -1) {
|
||||||
if (['text', 'title'].includes((rrx.Type || 'other').toLowerCase()) === false) { ipos++; }
|
if (['text', 'title'].includes((rrx.Type || 'other').toLowerCase()) === false) { ipos++; }
|
||||||
@@ -1147,41 +1213,10 @@ $inv.invSumUpdate = function () {
|
|||||||
$inv.rrw.call(tx);
|
$inv.rrw.call(tx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
//console.debug('%o', {
|
ba.push({ Id: rx.Id, nme: rx.Name, text: rx.text, itm: i, items: citems });
|
||||||
// f: b.find('tr.isum > td.isumval'), t: fnum(bnet, $rct.cst), n: bnet
|
|
||||||
//});
|
|
||||||
b.find('tr.isum > td.isumval').text(fnum(bnet, $rct.cst));
|
|
||||||
ba.push({ Id: rx.Id, nme: rx.Name, text: rx.text, itm: i, items: citems, netval: bnet });
|
|
||||||
});
|
});
|
||||||
let nonempty = tbl.find('tbody:not(.empty)').length;
|
let nonempty = tbl.find('tbody:not(.empty)').length;
|
||||||
bds.find('tr.isum').tC('hidden', nonempty < 2);
|
bds.find('tr.isum').tC('hidden', nonempty < 2);
|
||||||
//let fnet = $$.tdc('currency', $$.tr(ft, { class: 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text('Netto')]), fnum(sms.ttn, $rct.cst));
|
|
||||||
rwcy('Netto', sms.ttn);
|
|
||||||
if (p13b === false) {
|
|
||||||
$.each(sms.vat, (vi, vx) => {
|
|
||||||
//$$.tdc('currency vat', $$.tr(ft, { class: 'tvat' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text('Umsatzsteuer ' + vi)]), fnum(vx, $rct.cst));
|
|
||||||
rwcy($rct.vat + ' ' + vi, vx, 'tvat');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
sms.ttb = sms.ttn;
|
|
||||||
}
|
|
||||||
//let fsum = $$.tdc('currency', $$.tr(ft, { class: 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text('Summe')]), fnum(sms.ttb, $rct.cst));
|
|
||||||
rwcy('Summe', sms.ttb);
|
|
||||||
let itype = tbl.data().admin.type;
|
|
||||||
if (itype === 'i') {
|
|
||||||
fn($rct.note2);
|
|
||||||
fn($rct.note4);
|
|
||||||
} else if (itype === 'c') {
|
|
||||||
fn($rct.note2);
|
|
||||||
} else {
|
|
||||||
fn(string($rct.note3, [fnum((sms.tscn + sms.tscvat) * (tbl.data().admin.tax_servicerefund || 0), $rct.cst)])).aC('ntax');
|
|
||||||
fn($rct.note2);
|
|
||||||
fn(string($rct.note1, [fnum(sms.tscn + sms.tscvat, $rct.cst), fnum(sms.tscn, $rct.cst), fnum(sms.tscvat, $rct.cst)]));
|
|
||||||
}
|
|
||||||
if (p13b === true) {
|
|
||||||
fn($rct.note13b);
|
|
||||||
}
|
|
||||||
tbl.data('sms', sms);
|
|
||||||
tbl.data('bai', ba);
|
tbl.data('bai', ba);
|
||||||
/* Backend-authoritative seeding (ADR 0006): on the first calculation of an invoice
|
/* Backend-authoritative seeding (ADR 0006): on the first calculation of an invoice
|
||||||
draft (invSumUpdate is only bound for invoices, never reminders), hand the assembled
|
draft (invSumUpdate is only bound for invoices, never reminders), hand the assembled
|
||||||
@@ -1281,8 +1316,16 @@ $inv.t_fds_inv = () => {
|
|||||||
let tbl = $('div.invoice_layout table.invi');
|
let tbl = $('div.invoice_layout table.invi');
|
||||||
tbl.trigger('fds.inv');
|
tbl.trigger('fds.inv');
|
||||||
/* After any local item mutation, push the changed block(s) to the authoritative
|
/* After any local item mutation, push the changed block(s) to the authoritative
|
||||||
server session as granular deltas (invoice drafts only — reminders have no token). */
|
server session as granular deltas (invoice drafts only — reminders have no token,
|
||||||
if ((tbl.data('dtoken') || '') !== '') { $inv.d.syncChanged(tbl); }
|
nor a 'dseedpromise', so they correctly fall through to the no-op). While the initial
|
||||||
|
seed (inv/dopen) is still in flight there is no token yet either, but a 'dseedpromise'
|
||||||
|
is pending — wait for it instead of skipping the flush, otherwise an edit made in that
|
||||||
|
brief window is silently never sent. Returns the flush promise so callers that must
|
||||||
|
sequence a follow-up server delta after this flush (e.g. $inv.toSetPrice) can wait for
|
||||||
|
it instead of racing it. */
|
||||||
|
if ((tbl.data('dtoken') || '') !== '') { return $inv.d.syncChanged(tbl); }
|
||||||
|
let seeding = tbl.data('dseedpromise');
|
||||||
|
return seeding ? $.when(seeding).then(() => $inv.d.syncChanged(tbl)) : $.when();
|
||||||
};
|
};
|
||||||
$inv.sedit = () => {
|
$inv.sedit = () => {
|
||||||
$inv.sprev(true);
|
$inv.sprev(true);
|
||||||
@@ -1349,7 +1392,10 @@ $inv.itemToContract = function (rrx) {
|
|||||||
rrx = rrx || {};
|
rrx = rrx || {};
|
||||||
let oHtml = (e) => $$.d().append(e).html();
|
let oHtml = (e) => $$.d().append(e).html();
|
||||||
let type = (rrx.Type || '').toString().toLowerCase();
|
let type = (rrx.Type || '').toString().toLowerCase();
|
||||||
let ci = { id: (rrx.Id || '').toString(), type: type, title: '', desc: '', qty: '', price_net: '', total_net: (rrx.net_val || 0), vat: rrx.vat || '' };
|
/* ADR 0009: preserve null prices verbatim (null = empty cell, kept distinct from a real 0) so
|
||||||
|
block-mode nulled members and the set row round-trip unchanged when the block is re-posted. */
|
||||||
|
let nz = (x) => (x == null ? null : x);
|
||||||
|
let ci = { id: (rrx.Id || '').toString(), type: type, title: '', desc: '', qty: '', price_net: '', total_net: nz(rrx.net_val), vat: rrx.vat || '' };
|
||||||
if (rrx.co && rrx.co.typ === 'osum') {
|
if (rrx.co && rrx.co.typ === 'osum') {
|
||||||
/* combined single-sum line — the on-screen "title" is an HTML sub-table; render it as desc */
|
/* combined single-sum line — the on-screen "title" is an HTML sub-table; render it as desc */
|
||||||
ci.desc = rrx.co.t || '';
|
ci.desc = rrx.co.t || '';
|
||||||
@@ -1370,30 +1416,35 @@ $inv.itemToContract = function (rrx) {
|
|||||||
ci.desc = rrx.Note || '';
|
ci.desc = rrx.Note || '';
|
||||||
}
|
}
|
||||||
ci.qty = rrx.quantity || ((rrx.quantityhours || 0) !== 0 ? (fnum(rrx.quantityhours) + (rrx.UnitString ? ' ' + rrx.UnitString : '')) : '');
|
ci.qty = rrx.quantity || ((rrx.quantityhours || 0) !== 0 ? (fnum(rrx.quantityhours) + (rrx.UnitString ? ' ' + rrx.UnitString : '')) : '');
|
||||||
ci.price_net = (rrx.net || 0);
|
ci.price_net = nz(rrx.net);
|
||||||
ci.total_net = (rrx.net_val || 0);
|
ci.total_net = nz(rrx.net_val);
|
||||||
}
|
}
|
||||||
return ci;
|
return ci;
|
||||||
};
|
};
|
||||||
/* 3-way set-pricing display switch. Mirrors §13b: writes the choice onto admin.setmode,
|
/* 2-way set-pricing display switch (SetPrice / SetOnly). Mirrors §13b: writes the choice onto
|
||||||
which BuildInvoiceParams turns into the "setmode:<mode>" InvoiceOptions token the PDF reads. */
|
admin.setmode, which BuildInvoiceParams turns into the "setmode:<mode>" InvoiceOptions token
|
||||||
|
the PDF reads. ItemPrices ("Positionen mit Preis – Set als Überschrift") was removed: it was
|
||||||
|
always just the implicit pre-conversion default and is not a state the user can switch back to
|
||||||
|
once a set has been converted via the "Auf Setpreis umstellen" item switch (see $inv.toSetPrice /
|
||||||
|
InvoiceDraftEditService.ApplyItemSetPrice) — that conversion is one-way. */
|
||||||
$inv.ssetmode = () => {
|
$inv.ssetmode = () => {
|
||||||
let l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data();
|
let l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data();
|
||||||
d.admin = d.admin || {};
|
d.admin = d.admin || {};
|
||||||
let cur = (d.admin.setmode || 'setprice'), o;
|
let cur = (d.admin.setmode || 'setprice'), o;
|
||||||
let btn = (mode) => $$.dc('btn', $ict.setmo[mode]).tC('selected', cur === mode).click(() => { o.c.trigger('modal_close'); $inv.setSetmode(mode); });
|
let btn = (mode) => $$.dc('btn', $ict.setmo[mode]).tC('selected', cur === mode).click(() => { o.c.trigger('modal_close'); $inv.setSetmode(mode); });
|
||||||
let fr = $$.dc('choicefrm').append([btn('setprice'), btn('itemprices'), btn('setonly')]);
|
let fr = $$.dc('choicefrm').append([btn('setprice'), btn('setonly')]);
|
||||||
o = $ocms.dlg(fr, { width: 800 });
|
o = $ocms.dlg(fr, { width: 800 });
|
||||||
};
|
};
|
||||||
|
/* "Set mit Preis" / "Nur Set mit Preis" (ADR 0009): NOT a display toggle — an irreversible,
|
||||||
|
backend-authoritative mutation grouped by service-request block. The server inserts a dedicated
|
||||||
|
set row per block (block sum) and either nulls each member's price (setprice) or removes the
|
||||||
|
members (setonly); the normal draftReady refresh re-renders both via $inv.d.applyItems. The
|
||||||
|
browser only posts the chosen mode. Flush pending local edits first so a delayed block.replace
|
||||||
|
can't land after the conversion and overwrite it (same race guard as $inv.toSetPrice). */
|
||||||
$inv.setSetmode = (mode) => {
|
$inv.setSetmode = (mode) => {
|
||||||
let l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data();
|
if (confirm($rct.toSetMc || $rct.toSetPc) === false) { return; }
|
||||||
d.admin = d.admin || {};
|
let target = (mode === 'setonly') ? 'block.setonly' : 'block.setprice';
|
||||||
d.admin.setmode = mode; /* posted in admin -> BuildInvoiceParams writes setmode: into InvoiceOptions */
|
$.when($inv.t_fds_inv()).done(() => { $inv.d.sync({ Target: target }); });
|
||||||
d.inv = d.inv || {}; /* keep a local InvoiceOptions reflection in sync (cosmetic) */
|
|
||||||
let opts = (d.inv.InvoiceOptions || '').split(',').filter(x => x !== '' && x.indexOf('setmode:') !== 0);
|
|
||||||
if (mode && mode !== 'setprice') { opts.push('setmode:' + mode); }
|
|
||||||
d.inv.InvoiceOptions = opts.join(',');
|
|
||||||
$inv.d.sync({ Target: 'setmode', Value: mode });
|
|
||||||
};
|
};
|
||||||
$inv.sctp = () => {
|
$inv.sctp = () => {
|
||||||
let flds = $invcol.ctp;
|
let flds = $invcol.ctp;
|
||||||
@@ -1416,24 +1467,21 @@ $inv.sctp = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
/* Normalises the editor's working model into the exact field names the C# backend
|
/* Normalises the editor's working model into the exact field names the C# backend
|
||||||
(FdsInvoiceData.BuildInvoiceParams) reads, then returns the `invc` payload:
|
(FdsInvoiceData.BuildInvoiceParams) reads, then returns the `invc` payload. No totals/VAT
|
||||||
- balances/service sums come from `sms` (ttn/ttb), exposed on `new` as total_net/total_gross;
|
are computed here (ADR 0006/0008: the online editor performs no arithmetic at all) — the
|
||||||
- every VAT rate's net amount is exposed as new.vat_<rate>_net (the backend reads the highest);
|
backend session computes total_net/total_gross/vat itself (InvoiceDraftCalculator) once
|
||||||
|
opened from this payload, and BuildFdsData/BuildInvoiceParams reads them from there:
|
||||||
- new.invoicetitle -> new.title, new.loc -> new.provisionlocation, admin.paymentterms ->
|
- new.invoicetitle -> new.title, new.loc -> new.provisionlocation, admin.paymentterms ->
|
||||||
new.paymentterm, admin.CustomerId -> admin.customerid.
|
new.paymentterm, admin.CustomerId -> admin.customerid.
|
||||||
Originals are kept alongside; the source objects are not mutated. */
|
Originals are kept alongside; the source objects are not mutated. */
|
||||||
$inv.invcPayload = function (d) {
|
$inv.invcPayload = function (d) {
|
||||||
d = d || {};
|
d = d || {};
|
||||||
let sms = d.sms || {}, nw = $.extend({}, d.new), adm = $.extend({}, d.admin);
|
let nw = $.extend({}, d.new), adm = $.extend({}, d.admin);
|
||||||
nw.total_net = sms.ttn || 0;
|
|
||||||
nw.total_gross = sms.ttb || 0;
|
|
||||||
/* VAT (rate + amount) is taken by the backend straight from the posted sms.vat map
|
|
||||||
(FdsInvoiceData.HighestVat), so no per-rate new.vat_* keys are needed here. */
|
|
||||||
nw.title = (nw.invoicetitle != null ? nw.invoicetitle : (nw.title || ''));
|
nw.title = (nw.invoicetitle != null ? nw.invoicetitle : (nw.title || ''));
|
||||||
nw.provisionlocation = (nw.loc != null ? nw.loc : (nw.provisionlocation || ''));
|
nw.provisionlocation = (nw.loc != null ? nw.loc : (nw.provisionlocation || ''));
|
||||||
nw.paymentterm = (adm.paymentterms != null ? adm.paymentterms : (nw.paymentterm || ''));
|
nw.paymentterm = (adm.paymentterms != null ? adm.paymentterms : (nw.paymentterm || ''));
|
||||||
adm.customerid = (adm.customerid != null ? adm.customerid : adm.CustomerId);
|
adm.customerid = (adm.customerid != null ? adm.customerid : adm.CustomerId);
|
||||||
return { admin: adm, req: d.bai, sms: d.sms, new: nw };
|
return { admin: adm, req: d.bai, new: nw };
|
||||||
};
|
};
|
||||||
/* Zwischenspeichern and preview now run against the backend-authoritative session
|
/* Zwischenspeichern and preview now run against the backend-authoritative session
|
||||||
(ADR 0006): no full-invoice re-upload — the cached draft is flushed / rendered by
|
(ADR 0006): no full-invoice re-upload — the cached draft is flushed / rendered by
|
||||||
@@ -1447,20 +1495,11 @@ $inv.rReload = () => {
|
|||||||
$inv.cInv2({ id: s.search });
|
$inv.cInv2({ id: s.search });
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
};
|
};
|
||||||
$inv.quantChange = function (i) {
|
/* No client-side arithmetic (ADR 0006/0008): quantity/price/VAT edits are posted as raw values
|
||||||
//console.debug({ t: this, i: i });
|
and the server (InvoiceDraftCalculator.RecomputeLineValues) computes net_val/vat_val/
|
||||||
let t = $(this), f = t.closest('form'), fi = {}, pf = (i) => parseFloat(i.toString().replace('%', '').replace(',', '.')), rtp = (num) => num.toFixed(2);
|
svcnet_val/svcvat_val. The fields are left as entered here; the authoritative values are
|
||||||
f.find(':input').each((i, e) => { fi[$(e).attr('name')] = $(e); });
|
pushed back onto the row by $inv.d.applyItems once the change reaches the backend. */
|
||||||
let qv = parseInt(fi.quantityhours.val() || '0'), nv = pf(fi.net.val() || '0'), vat = pf(fi.vat.val()) * 0.01;
|
$inv.quantChange = function (i) { };
|
||||||
if (qv > 0 && nv > 0) {
|
|
||||||
fi.net_val.val(rtp(qv * nv));
|
|
||||||
fi.vat_val.val(rtp(qv * nv * vat));
|
|
||||||
if (['Service'].includes(fi.Type.val())) {
|
|
||||||
fi.svcnet_val.val(rtp(qv * nv));
|
|
||||||
fi.svcvat_val.val(rtp(qv * nv * vat));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
$inv.storno = function (id, fds) {
|
$inv.storno = function (id, fds) {
|
||||||
let o, fr = $$.dc('choicefrm').append([
|
let o, fr = $$.dc('choicefrm').append([
|
||||||
$$.dc('btn', 'Storno ohne Details').click({ id: id, mode: 'simple' }, (ev) => { o.c.trigger('modal_close'); $inv.cSt(ev.data); })
|
$$.dc('btn', 'Storno ohne Details').click({ id: id, mode: 'simple' }, (ev) => { o.c.trigger('modal_close'); $inv.cSt(ev.data); })
|
||||||
|
|||||||
@@ -43,7 +43,6 @@
|
|||||||
setm: 'Set-Preisanzeige',
|
setm: 'Set-Preisanzeige',
|
||||||
setmo: {
|
setmo: {
|
||||||
setprice: 'Set mit Preis – Positionen ohne Preis',
|
setprice: 'Set mit Preis – Positionen ohne Preis',
|
||||||
itemprices: 'Positionen mit Preis – Set als Überschrift',
|
|
||||||
setonly: 'Nur Set mit Preis – Positionen ausgeblendet'
|
setonly: 'Nur Set mit Preis – Positionen ausgeblendet'
|
||||||
},
|
},
|
||||||
ctp: 'Ansprechpartner festlegen',
|
ctp: 'Ansprechpartner festlegen',
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
combP: 'Positionen zusammenfassen',
|
combP: 'Positionen zusammenfassen',
|
||||||
toSetP: 'Auf Setpreis umstellen (Positionen ohne Preis)',
|
toSetP: 'Auf Setpreis umstellen (Positionen ohne Preis)',
|
||||||
toSetPc: 'Der Set-Preis wird auf die Summe seiner Positionen gesetzt und die Positionen selbst verlieren ihren Preis. Das ist nicht rückgängig zu machen (der Betrag kann danach nur noch manuell geändert werden). Fortfahren?',
|
toSetPc: 'Der Set-Preis wird auf die Summe seiner Positionen gesetzt und die Positionen selbst verlieren ihren Preis. Das ist nicht rückgängig zu machen (der Betrag kann danach nur noch manuell geändert werden). Fortfahren?',
|
||||||
|
toSetMc: 'Jeder Leistungsauftrag wird zu einer Set-Position mit der Summe als Preis zusammengefasst. Das ist nicht rückgängig zu machen (die Set-Preise können danach nur noch manuell geändert werden). Fortfahren?',
|
||||||
iSum: 'Zwischensumme',
|
iSum: 'Zwischensumme',
|
||||||
dtRel: 'Freigegeben am: ',
|
dtRel: 'Freigegeben am: ',
|
||||||
dtCr: 'Erstellt am: ',
|
dtCr: 'Erstellt am: ',
|
||||||
|
|||||||
@@ -577,7 +577,7 @@ $ocms.postXT = function (options) {
|
|||||||
if ((typeof options.contentType === 'boolean' ? options.contentType === false : false) === true) {
|
if ((typeof options.contentType === 'boolean' ? options.contentType === false : false) === true) {
|
||||||
ajo.contentType = false; /* needed for file uploads */
|
ajo.contentType = false; /* needed for file uploads */
|
||||||
}
|
}
|
||||||
$.ajax(ajo);
|
return $.ajax(ajo);
|
||||||
};
|
};
|
||||||
$ocms.cex_timer = function () {
|
$ocms.cex_timer = function () {
|
||||||
if (!$ocms.cexi) {
|
if (!$ocms.cexi) {
|
||||||
|
|||||||
@@ -198,7 +198,15 @@ if (!Element.prototype.closest) {
|
|||||||
_onPress: function (e) {
|
_onPress: function (e) {
|
||||||
if (!e) { return; }
|
if (!e) { return; }
|
||||||
function op(tgt) {
|
function op(tgt) {
|
||||||
if (tgt && tgt.parentNode === this._container && (this._dragHandleClass === '' || e.target.className.indexOf(this._dragHandleClass) > -1) && tgt.className.indexOf("nosort") < 0) {
|
// Match the drag handle by real CSS class token, not substring. The old
|
||||||
|
// e.target.className.indexOf('ico') matched every Bootstrap "glyph[ico]n" icon,
|
||||||
|
// so clicking any icon button (edit/delete/set-price…) inside a sortable row
|
||||||
|
// started a zero-distance drag that swallowed the button's own click. Using
|
||||||
|
// closest('.<handle>') restricts dragging to the dedicated handle element (e.g.
|
||||||
|
// the ".ico" move button) and its descendants only.
|
||||||
|
var isHandle = this._dragHandleClass === '' ||
|
||||||
|
(e.target.closest && e.target.closest('.' + this._dragHandleClass) != null);
|
||||||
|
if (tgt && tgt.parentNode === this._container && isHandle && tgt.className.indexOf("nosort") < 0) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this._dragging = true;
|
this._dragging = true;
|
||||||
this._click = getPoint(e);
|
this._click = getPoint(e);
|
||||||
|
|||||||
+201
-162
@@ -36,6 +36,7 @@ let $rct = {
|
|||||||
combP: 'Positionen zusammenfassen',
|
combP: 'Positionen zusammenfassen',
|
||||||
toSetP: 'Auf Setpreis umstellen (Positionen ohne Preis)',
|
toSetP: 'Auf Setpreis umstellen (Positionen ohne Preis)',
|
||||||
toSetPc: 'Der Set-Preis wird auf die Summe seiner Positionen gesetzt und die Positionen selbst verlieren ihren Preis. Das ist nicht rückgängig zu machen (der Betrag kann danach nur noch manuell geändert werden). Fortfahren?',
|
toSetPc: 'Der Set-Preis wird auf die Summe seiner Positionen gesetzt und die Positionen selbst verlieren ihren Preis. Das ist nicht rückgängig zu machen (der Betrag kann danach nur noch manuell geändert werden). Fortfahren?',
|
||||||
|
toSetMc: 'Jeder Leistungsauftrag wird zu einer Set-Position mit der Summe als Preis zusammengefasst. Das ist nicht rückgängig zu machen (die Set-Preise können danach nur noch manuell geändert werden). Fortfahren?',
|
||||||
iSum: 'Zwischensumme',
|
iSum: 'Zwischensumme',
|
||||||
dtRel: 'Freigegeben am: ',
|
dtRel: 'Freigegeben am: ',
|
||||||
dtCr: 'Erstellt am: ',
|
dtCr: 'Erstellt am: ',
|
||||||
@@ -151,7 +152,6 @@ let $ict = {
|
|||||||
setm: 'Set-Preisanzeige',
|
setm: 'Set-Preisanzeige',
|
||||||
setmo: {
|
setmo: {
|
||||||
setprice: 'Set mit Preis – Positionen ohne Preis',
|
setprice: 'Set mit Preis – Positionen ohne Preis',
|
||||||
itemprices: 'Positionen mit Preis – Set als Überschrift',
|
|
||||||
setonly: 'Nur Set mit Preis – Positionen ausgeblendet'
|
setonly: 'Nur Set mit Preis – Positionen ausgeblendet'
|
||||||
},
|
},
|
||||||
ctp: 'Ansprechpartner festlegen',
|
ctp: 'Ansprechpartner festlegen',
|
||||||
@@ -673,10 +673,13 @@ $inv.d = {
|
|||||||
return h;
|
return h;
|
||||||
},
|
},
|
||||||
/* Seed the authoritative server session from the assembled editor payload. Called
|
/* Seed the authoritative server session from the assembled editor payload. Called
|
||||||
once from invSumUpdate on open; thereafter the backend is the source of truth. */
|
once from invSumUpdate on open; thereafter the backend is the source of truth.
|
||||||
|
The in-flight ajax promise is stashed on 'dseedpromise' so a delta sent (e.g. via
|
||||||
|
a context-menu action) before seeding has actually completed can wait for the token
|
||||||
|
instead of silently no-op'ing (see $inv.d.sync). */
|
||||||
seed: function (payload) {
|
seed: function (payload) {
|
||||||
let l = $inv.d.layout(); l.aC('freeze');
|
let l = $inv.d.layout(); l.aC('freeze');
|
||||||
$ocms.postXT({
|
let p = $ocms.postXT({
|
||||||
url: $ocms.url('inv/dopen'), data: { payload: JSON.stringify(payload) }, success: (r) => {
|
url: $ocms.url('inv/dopen'), data: { payload: JSON.stringify(payload) }, success: (r) => {
|
||||||
$inv.d.tbl().data('dtoken', r.token).data('dver', r.version).data('dhashes', $inv.d.hashes()).data('dorder', $inv.d.order());
|
$inv.d.tbl().data('dtoken', r.token).data('dver', r.version).data('dhashes', $inv.d.hashes()).data('dorder', $inv.d.order());
|
||||||
$fis.draft.bind(r.token, {
|
$fis.draft.bind(r.token, {
|
||||||
@@ -685,8 +688,10 @@ $inv.d = {
|
|||||||
onClosed: (reason) => $inv.d.closed(reason)
|
onClosed: (reason) => $inv.d.closed(reason)
|
||||||
});
|
});
|
||||||
$inv.d.refresh();
|
$inv.d.refresh();
|
||||||
}, error: () => { l.rC('freeze'); }, complete: () => { $inv.d.tbl().removeData('dseeding'); }
|
}, error: () => { l.rC('freeze'); }, complete: () => { $inv.d.tbl().removeData('dseeding').removeData('dseedpromise'); }
|
||||||
});
|
});
|
||||||
|
$inv.d.tbl().data('dseedpromise', p);
|
||||||
|
return p;
|
||||||
},
|
},
|
||||||
/* Re-fetch the authoritative state and render the totals footer + validation from it. */
|
/* Re-fetch the authoritative state and render the totals footer + validation from it. */
|
||||||
refresh: function (cb) {
|
refresh: function (cb) {
|
||||||
@@ -700,11 +705,21 @@ $inv.d = {
|
|||||||
},
|
},
|
||||||
applyState: function (state) {
|
applyState: function (state) {
|
||||||
let tbl = $inv.d.tbl(); if (tbl.length < 1) { return; }
|
let tbl = $inv.d.tbl(); if (tbl.length < 1) { return; }
|
||||||
|
/* Concurrent refreshes (one per flushed block plus one for a follow-up delta such as
|
||||||
|
item.setprice, and SignalR draftReady on top) can have their dstate responses arrive
|
||||||
|
out of order. Applying a response older than the version already rendered would
|
||||||
|
silently roll back a just-applied server-side mutation (e.g. a set-price conversion
|
||||||
|
reverting to its pre-conversion 0/individually-priced state) — so stale responses are
|
||||||
|
dropped here instead of rendered. */
|
||||||
|
let curVer = tbl.data('dver');
|
||||||
|
if (typeof curVer === 'number' && typeof state.version === 'number' && state.version < curVer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
tbl.data('dver', state.version).data('serverSums', state.sums);
|
tbl.data('dver', state.version).data('serverSums', state.sums);
|
||||||
$inv.d.footer(tbl, state.sums || {}, state.admin || {});
|
$inv.d.footer(tbl, state.sums || {}, state.admin || {});
|
||||||
$inv.d.notes(state.notes || []);
|
$inv.d.notes(state.notes || []);
|
||||||
$inv.d.validation(state.validation || []);
|
$inv.d.validation(state.validation || []);
|
||||||
$inv.d.applyPositions(tbl, state.req || []);
|
$inv.d.applyItems(tbl, state.req || []);
|
||||||
$inv.d.applySetDisplay(tbl, state.setDisplay || {});
|
$inv.d.applySetDisplay(tbl, state.setDisplay || {});
|
||||||
/* Keep the menu's "Set-Preisanzeige" entry in sync with the authoritative admin.setmode:
|
/* Keep the menu's "Set-Preisanzeige" entry in sync with the authoritative admin.setmode:
|
||||||
hides it right after a mode switch, and restores it if a discard reverted the draft to
|
hides it right after a mode switch, and restores it if a discard reverted the draft to
|
||||||
@@ -724,19 +739,60 @@ $inv.d = {
|
|||||||
/* Push the server's authoritative position numbers back onto the rendered rows so the online
|
/* Push the server's authoritative position numbers back onto the rendered rows so the online
|
||||||
editor and the PDF preview always agree (the server numbers priced lines continuously; the
|
editor and the PDF preview always agree (the server numbers priced lines continuously; the
|
||||||
browser must not keep its own numbering). Only the position cell is touched — no re-render. */
|
browser must not keep its own numbering). Only the position cell is touched — no re-render. */
|
||||||
applyPositions: function (tbl, req) {
|
/* Reconciles each block's rendered rows against the authoritative server line list
|
||||||
(req || []).forEach((b) => (b && b.itm || []).forEach((co) => {
|
(`req[].itm`, the co shape). The server may not only change a line's values but also
|
||||||
if (!co || (co.id || '') === '') { return; }
|
ADD lines (the block set row inserted by "Set mit Preis"/block.setprice) and REMOVE lines
|
||||||
let cell = tbl.find('#itm' + co.id + ' td.keep').first();
|
("Nur Set mit Preis"/block.setonly), and reorder them — the browser never computes any of
|
||||||
if (cell.length) { cell.text(co.p != null ? co.p : ''); }
|
that (ADR 0006/0008/0009), it just mirrors the server. For each server line: patch/insert
|
||||||
}));
|
the matching row, preserving null price fields verbatim (ADR 0009: null = empty cell, kept
|
||||||
|
distinct from 0). Rows the server no longer lists are removed; order follows the server.
|
||||||
|
Position numbers, net/VAT and the set-row emphasis all come from rrw off the mirrored data. */
|
||||||
|
applyItems: function (tbl, req) {
|
||||||
|
let val = (x) => (x === undefined ? null : x); // JSON null stays null; absent -> null
|
||||||
|
(req || []).forEach((b) => {
|
||||||
|
if (!b) { return; }
|
||||||
|
let bid = (b.Id || '').toString(); if (bid === '') { return; }
|
||||||
|
let bdy = tbl.children('tbody').filter((i, e) => (($(e).data() || {}).Id || '').toString() === bid).first();
|
||||||
|
if (bdy.length < 1) { return; }
|
||||||
|
let itms = b.itm || [], serverIds = {};
|
||||||
|
let title = bdy.find('tr.title').first();
|
||||||
|
let prev = title.length ? title : null;
|
||||||
|
itms.forEach((co) => {
|
||||||
|
let id = (co.id || '').toString(); if (id === '') { return; }
|
||||||
|
serverIds[id] = true;
|
||||||
|
let rw = bdy.find('#itm' + id).first(), isNew = rw.length < 1;
|
||||||
|
if (isNew) { rw = $$.tr(bdy, { id: 'itm' + id, class: 'itm' }); }
|
||||||
|
let dta = rw.data() || {};
|
||||||
|
let changed = isNew || (dta.Type || '') !== (co.typ || '')
|
||||||
|
|| dta.net !== val(co.v) || dta.net_val !== val(co.vt) || dta.vat_val !== val(co.vv)
|
||||||
|
|| dta.svcnet_val !== val(co.vs) || dta.svcvat_val !== val(co.vsv)
|
||||||
|
|| (dta.position || '') !== (co.p != null ? co.p : '');
|
||||||
|
$.extend(dta, {
|
||||||
|
Id: id, Type: co.typ,
|
||||||
|
net: val(co.v), net_val: val(co.vt), vat_val: val(co.vv),
|
||||||
|
svcnet_val: val(co.vs), svcvat_val: val(co.vsv),
|
||||||
|
vat: co.vat, position: (co.p != null ? co.p : ''),
|
||||||
|
SetItmId: (co.SetItmId != null ? co.SetItmId : null)
|
||||||
|
});
|
||||||
|
if (isNew) { $.extend(dta, { htmltext: co.t, quantity: co.q }); } // text only for server-created rows (e.g. the set row)
|
||||||
|
rw.data(dta);
|
||||||
|
if (prev) { if (rw.prev()[0] !== prev[0]) { rw.insertAfter(prev); } } // keep server order, but don't churn already-ordered rows
|
||||||
|
else if (bdy.children().first()[0] !== rw[0]) { bdy.prepend(rw); }
|
||||||
|
prev = rw;
|
||||||
|
if (changed) { $inv.rrw.call(rw); }
|
||||||
|
});
|
||||||
|
bdy.find('tr.itm').each((i, tr) => {
|
||||||
|
let id = (($(tr).data() || {}).Id || '').toString();
|
||||||
|
if (id !== '' && !serverIds[id]) { $(tr).remove(); } // dropped server-side (block.setonly)
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
/* Applies the backend-authoritative set-pricing display flags (see BuildSetDisplay /
|
/* Applies the backend-authoritative set-pricing display flags (see BuildSetDisplay /
|
||||||
InvoiceSetPricing) onto the rendered rows: blanks the price/total cells for lines the
|
InvoiceSetPricing) onto the rendered rows: blanks the price/total cells for lines the
|
||||||
server says should show no price (set members in SetPrice mode, the set header in
|
server says should show no price (set members in SetPrice/SetOnly mode), and emphasises
|
||||||
ItemPrices mode), and emphasises the set header line — so the online editor always
|
the set header line — so the online editor always shows exactly what the PDF will print,
|
||||||
shows exactly what the PDF will print, without duplicating the pricing rules client-side.
|
without duplicating the pricing rules client-side. Rows not present in the map (no set in
|
||||||
Rows not present in the map (no set in that block, or standalone items) are left untouched. */
|
that block, or standalone items) are left untouched. */
|
||||||
applySetDisplay: function (tbl, setDisplay) {
|
applySetDisplay: function (tbl, setDisplay) {
|
||||||
tbl.find('tr.itm').each((i, tr) => {
|
tbl.find('tr.itm').each((i, tr) => {
|
||||||
let rw = $(tr), dta = rw.data() || {}, id = (dta.Id || '').toString();
|
let rw = $(tr), dta = rw.data() || {}, id = (dta.Id || '').toString();
|
||||||
@@ -748,11 +804,23 @@ $inv.d = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
/* Send one change to the server; the draftReady signal and this success both refresh. */
|
/* Send one change to the server; the draftReady signal and this success both refresh.
|
||||||
|
Returns the underlying ajax promise so callers that must not race a follow-up delta
|
||||||
|
against this one (e.g. $inv.toSetPrice after a flush) can chain on it.
|
||||||
|
If the initial seeding (inv/dopen) is still in flight (no token yet, but a
|
||||||
|
'dseedpromise' is pending), the delta previously vanished silently — the user saw no
|
||||||
|
network activity at all when clicking a context-menu action right after opening the
|
||||||
|
editor. Now the delta waits for the seed to finish and retries against the freshly
|
||||||
|
issued token instead of being dropped. */
|
||||||
sync: function (delta) {
|
sync: function (delta) {
|
||||||
let t = $inv.d.token(); if (t === '') { return; }
|
let t = $inv.d.token();
|
||||||
|
if (t === '') {
|
||||||
|
let seeding = $inv.d.tbl().data('dseedpromise');
|
||||||
|
if (seeding) { return $.when(seeding).then(() => $inv.d.sync(delta)); }
|
||||||
|
return $.when();
|
||||||
|
}
|
||||||
$inv.d.layout().aC('freeze');
|
$inv.d.layout().aC('freeze');
|
||||||
$ocms.postXT({
|
return $ocms.postXT({
|
||||||
url: $ocms.url('inv/dpatch'), data: { token: t, delta: JSON.stringify(delta) },
|
url: $ocms.url('inv/dpatch'), data: { token: t, delta: JSON.stringify(delta) },
|
||||||
success: () => { $inv.d.refresh(); },
|
success: () => { $inv.d.refresh(); },
|
||||||
error: (xhr) => { $inv.d.layout().rC('freeze'); if (xhr && xhr.status === 410) { $inv.d.closed('expired'); } }
|
error: (xhr) => { $inv.d.layout().rC('freeze'); if (xhr && xhr.status === 410) { $inv.d.closed('expired'); } }
|
||||||
@@ -763,22 +831,29 @@ $inv.d = {
|
|||||||
/* Diff the freshly-rebuilt blocks against the last-synced state and send only the
|
/* Diff the freshly-rebuilt blocks against the last-synced state and send only the
|
||||||
changed/removed blocks as granular block.replace / block.remove deltas. A pure section
|
changed/removed blocks as granular block.replace / block.remove deltas. A pure section
|
||||||
reorder (same blocks, new sequence) changes no block hash, so it is sent separately as a
|
reorder (same blocks, new sequence) changes no block hash, so it is sent separately as a
|
||||||
block.order delta; the server reorders the cache, renumbers positions and pushes them back. */
|
block.order delta; the server reorders the cache, renumbers positions and pushes them back.
|
||||||
|
No early return on an empty token here: sync() itself waits for an in-flight seed
|
||||||
|
(dseedpromise) instead of silently dropping the delta, so edits made in the brief window
|
||||||
|
before seeding completes are not lost. */
|
||||||
syncChanged: function (tbl) {
|
syncChanged: function (tbl) {
|
||||||
if (($inv.d.token()) === '') { return; }
|
let bai = tbl.data('bai') || [], prev = tbl.data('dhashes') || {}, next = {}, changed = [], removed = [], pending = [];
|
||||||
let bai = tbl.data('bai') || [], prev = tbl.data('dhashes') || {}, next = {}, changed = [], removed = [];
|
|
||||||
$.each(bai, (i, b) => { let id = (b.Id || '').toString(), h = JSON.stringify(b); next[id] = h; if (prev[id] !== h) { changed.push(b); } });
|
$.each(bai, (i, b) => { let id = (b.Id || '').toString(), h = JSON.stringify(b); next[id] = h; if (prev[id] !== h) { changed.push(b); } });
|
||||||
$.each(prev, (id) => { if (next[id] === undefined) { removed.push(id); } });
|
$.each(prev, (id) => { if (next[id] === undefined) { removed.push(id); } });
|
||||||
let order = $inv.d.order(), prevOrder = tbl.data('dorder') || [];
|
let order = $inv.d.order(), prevOrder = tbl.data('dorder') || [];
|
||||||
tbl.data('dhashes', next).data('dorder', order);
|
tbl.data('dhashes', next).data('dorder', order);
|
||||||
changed.forEach((b) => $inv.d.sync({ Target: 'block.replace', Ref: (b.Id || '').toString(), Value: b }));
|
changed.forEach((b) => pending.push($inv.d.sync({ Target: 'block.replace', Ref: (b.Id || '').toString(), Value: b })));
|
||||||
removed.forEach((id) => $inv.d.sync({ Target: 'block.remove', Ref: id }));
|
removed.forEach((id) => pending.push($inv.d.sync({ Target: 'block.remove', Ref: id })));
|
||||||
let sameSet = prevOrder.length === order.length && prevOrder.slice().sort().join(',') === order.slice().sort().join(',');
|
let sameSet = prevOrder.length === order.length && prevOrder.slice().sort().join(',') === order.slice().sort().join(',');
|
||||||
if (sameSet && prevOrder.join(',') !== order.join(',')) { $inv.d.sync({ Target: 'block.order', Value: order }); }
|
if (sameSet && prevOrder.join(',') !== order.join(',')) { pending.push($inv.d.sync({ Target: 'block.order', Value: order })); }
|
||||||
|
/* $.when(...pending) so a caller (e.g. toSetPrice) can wait until every flushed block
|
||||||
|
has actually reached the server before sending a further delta that depends on it —
|
||||||
|
otherwise a delayed block.replace carrying pre-conversion values can land after and
|
||||||
|
silently overwrite a just-applied server-side mutation (e.g. item.setprice). */
|
||||||
|
return $.when.apply($, pending).promise();
|
||||||
},
|
},
|
||||||
/* Map an inline recipient field to its delta target and send it. */
|
/* Map an inline recipient field to its delta target and send it. No token-guard here for the
|
||||||
|
same reason as syncChanged: sync() waits for an in-flight seed instead of dropping the edit. */
|
||||||
syncField: function (nme, val) {
|
syncField: function (nme, val) {
|
||||||
if ($inv.d.token() === '') { return; }
|
|
||||||
let map = { invoicetitle: 'title', invoiceaddress: 'address', invoiceemail: 'email', loc: 'provisionlocation', provisionlocation: 'provisionlocation', provisionperiod: 'provisionperiod' };
|
let map = { invoicetitle: 'title', invoiceaddress: 'address', invoiceemail: 'email', loc: 'provisionlocation', provisionlocation: 'provisionlocation', provisionperiod: 'provisionperiod' };
|
||||||
let target = map[nme]; if (!target) { return; }
|
let target = map[nme]; if (!target) { return; }
|
||||||
$inv.d.sync({ Target: target, Value: val });
|
$inv.d.sync({ Target: target, Value: val });
|
||||||
@@ -933,6 +1008,12 @@ $inv.rd = {
|
|||||||
},
|
},
|
||||||
applyState: function (state) {
|
applyState: function (state) {
|
||||||
let tbl = $inv.rd.tbl(); if (tbl.length < 1) { return; }
|
let tbl = $inv.rd.tbl(); if (tbl.length < 1) { return; }
|
||||||
|
/* See $inv.d.applyState — drop stale/out-of-order dstate responses so an older refresh
|
||||||
|
can never roll back a newer one's totals/validation. */
|
||||||
|
let curVer = tbl.data('rdver');
|
||||||
|
if (typeof curVer === 'number' && typeof state.version === 'number' && state.version < curVer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
tbl.data('rdver', state.version).data('serverSums', state.sums).data('remid', state.remid || '');
|
tbl.data('rdver', state.version).data('serverSums', state.sums).data('remid', state.remid || '');
|
||||||
$inv.rd.footer(tbl, state.sums || {});
|
$inv.rd.footer(tbl, state.sums || {});
|
||||||
$inv.rd.notes(state.notes || {});
|
$inv.rd.notes(state.notes || {});
|
||||||
@@ -1492,6 +1573,8 @@ $inv.eHtml = function (ev) {
|
|||||||
}
|
}
|
||||||
$ocms.dlgform(flds, sets);
|
$ocms.dlgform(flds, sets);
|
||||||
};
|
};
|
||||||
|
/* No client-side arithmetic (ADR 0006/0008): only the chosen VAT rate is stored on each row;
|
||||||
|
the server (InvoiceDraftCalculator.RecomputeLineValues) recomputes vat_val/svcvat_val from it. */
|
||||||
$inv.setVat = function (ev) {
|
$inv.setVat = function (ev) {
|
||||||
let t = $(this), thisrow = ev.data;
|
let t = $(this), thisrow = ev.data;
|
||||||
let vat = prompt($rct.rqV);
|
let vat = prompt($rct.rqV);
|
||||||
@@ -1504,12 +1587,6 @@ $inv.setVat = function (ev) {
|
|||||||
thisrow.siblings('.itm').each(function () {
|
thisrow.siblings('.itm').each(function () {
|
||||||
let rwi = $(this), dta = rwi.data();
|
let rwi = $(this), dta = rwi.data();
|
||||||
dta.vat = fnum(vat, { style: 'percent' }).replace(' ', '');
|
dta.vat = fnum(vat, { style: 'percent' }).replace(' ', '');
|
||||||
if ((dta.net_val || 0) > 0) {
|
|
||||||
dta.vat_val = dta.net_val * vat;
|
|
||||||
}
|
|
||||||
if ((dta.svcnet_val || 0) > 0) {
|
|
||||||
dta.svcvat_val = dta.svcnet_val * vat;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
$inv.t_fds_inv();
|
$inv.t_fds_inv();
|
||||||
}
|
}
|
||||||
@@ -1569,12 +1646,14 @@ $inv.rrw = function () {
|
|||||||
if (ph === false) {
|
if (ph === false) {
|
||||||
bc.unshift($$.dc('ibtn edit', { title: $rct.cP }).append(gi('pencil')).click(rw, $inv.eRow));
|
bc.unshift($$.dc('ibtn edit', { title: $rct.cP }).append(gi('pencil')).click(rw, $inv.eRow));
|
||||||
bc.push($$.dc('ibtn del', { title: $rct.dR }).append(gi('trash')).click(function (e) { if (confirm($rct.cD)) { rw.remove(); $inv.t_fds_inv(); } }));
|
bc.push($$.dc('ibtn del', { title: $rct.dR }).append(gi('trash')).click(function (e) { if (confirm($rct.cD)) { rw.remove(); $inv.t_fds_inv(); } }));
|
||||||
/* "Auf Setpreis umstellen": only offered on a Set header item that is not yet
|
/* "Auf Setpreis umstellen": offered on a Set header item that does not yet carry a
|
||||||
priced from its members (fds__prepInvoice's [SetItmId] is null exactly for a
|
price. fds__prepInvoice's [SetItmId] now anchors on the still-unconverted (price 0)
|
||||||
Set header — either already set-priced, or not yet converted). Irreversible:
|
header, so the header row's own SetItmId self-references its own id rather than being
|
||||||
sets this row's price to the sum of its members (rows whose [SetItmId] equals
|
null — the real signal for showing this button is still the header's own price/net_val
|
||||||
this row's id) and clears each member's price. */
|
being 0 (once converted it carries a nonzero price and the button disappears).
|
||||||
if ((dta.Type || '').toLowerCase() === 'set' && (dta.SetItmId || '') === '') {
|
Irreversible: the actual sum/clear mutation happens server-side (backend-authoritative,
|
||||||
|
see InvoiceDraftEditService.ApplyItemSetPrice) via the "item.setprice" patch target. */
|
||||||
|
if ((dta.Type || '').toLowerCase() === 'set' && (dta.net_val || 0) === 0) {
|
||||||
bc.push($$.dc('ibtn tosetp', { title: $rct.toSetP }).append(gi('scale')).click(rw, $inv.toSetPrice));
|
bc.push($$.dc('ibtn tosetp', { title: $rct.toSetP }).append(gi('scale')).click(rw, $inv.toSetPrice));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1584,7 +1663,7 @@ $inv.rrw = function () {
|
|||||||
} else if (rw.is('.itm.osum') === true) {
|
} else if (rw.is('.itm.osum') === true) {
|
||||||
co = { invrqid: dta.InvRqId, id: 'osum' + rw.index(), typ: 'osum', p: '', q: null, t: oHtml(dta.tbl.tbl), tt: null, v: null, vt: dta.net_val, vs: dta.svcnet_val, vat: dta.vat, vv: dta.vat_val, vsv: dta.svcvat_val, det: false };
|
co = { invrqid: dta.InvRqId, id: 'osum' + rw.index(), typ: 'osum', p: '', q: null, t: oHtml(dta.tbl.tbl), tt: null, v: null, vt: dta.net_val, vs: dta.svcnet_val, vat: dta.vat, vv: dta.vat_val, vsv: dta.svcvat_val, det: false };
|
||||||
} else {
|
} else {
|
||||||
co = { invrqid: dta.InvRqId, id: dta.Id || '', typ: dta.Type || 'other', p: '', q: null, t: '', tt: null, v: null, vt: dta.net_val, vs: dta.svcnet_val, vat: dta.vat, vv: dta.vat_val, vsv: dta.svcvat_val, det: (dta.Note || '') !== '' && hn === false };
|
co = { invrqid: dta.InvRqId, id: dta.Id || '', typ: dta.Type || 'other', p: '', q: null, t: '', tt: null, v: null, vt: dta.net_val, vs: dta.svcnet_val, vat: dta.vat, vv: dta.vat_val, vsv: dta.svcvat_val, det: (dta.Note || '') !== '' && hn === false, SetItmId: dta.SetItmId || null };
|
||||||
$$.dc('ibtn ico move', axf, { title: $rct.mR }); /* should be the last added */
|
$$.dc('ibtn ico move', axf, { title: $rct.mR }); /* should be the last added */
|
||||||
|
|
||||||
co.p = dta.position || (dta.SortOrder || '');
|
co.p = dta.position || (dta.SortOrder || '');
|
||||||
@@ -1594,7 +1673,7 @@ $inv.rrw = function () {
|
|||||||
co.t = dta.htmltext || (((dta.NameOrNumber || '').substr(0, 1) !== '#' ? oHtml($$[0]('p').text(dta.NameOrNumber)) : '') + (dta.Note || ''));
|
co.t = dta.htmltext || (((dta.NameOrNumber || '').substr(0, 1) !== '#' ? oHtml($$[0]('p').text(dta.NameOrNumber)) : '') + (dta.Note || ''));
|
||||||
} else {
|
} else {
|
||||||
co.tt = co.det ? '' : $$.s(dta.Note || '').text();
|
co.tt = co.det ? '' : $$.s(dta.Note || '').text();
|
||||||
co.q = dta.quantity || (fnum(dta.quantityhours) + ' ' + (dta.UnitString || ''));
|
co.q = dta.quantity || ((dta.quantityhours || 0) !== 0 ? (fnum(dta.quantityhours) + ' ' + (dta.UnitString || '')) : ''); /* guard: no quantityhours -> blank, not fnum(undefined)="NaN" (e.g. the block set row) */
|
||||||
co.t = dta.htmltext ||(co.det ? (oHtml($$.s(dta.NameOrNumber || '')) + oHtml($$.dc('desc').html(dta.Note))) : oHtml($$.s(dta.NameOrNumber || '')));
|
co.t = dta.htmltext ||(co.det ? (oHtml($$.s(dta.NameOrNumber || '')) + oHtml($$.dc('desc').html(dta.Note))) : oHtml($$.s(dta.NameOrNumber || '')));
|
||||||
co.v = dta.net
|
co.v = dta.net
|
||||||
co.vt = dta.net_val
|
co.vt = dta.net_val
|
||||||
@@ -1613,75 +1692,62 @@ $inv.rrw = function () {
|
|||||||
tda.push($$.td(rw, { colspan: 4 }).append(co.t));
|
tda.push($$.td(rw, { colspan: 4 }).append(co.t));
|
||||||
} else {
|
} else {
|
||||||
Array.prototype.push.apply(tda, co.q ? [$$.tdc('keep').text(co.q)] : []);
|
Array.prototype.push.apply(tda, co.q ? [$$.tdc('keep').text(co.q)] : []);
|
||||||
|
/* ADR 0009: a null/undefined price renders an EMPTY cell (block-mode "Set mit Preis"
|
||||||
|
members, and the set row's own unit-price), distinct from a real 0,00 €. */
|
||||||
|
let cur = (x) => (x == null ? '' : fnum(x, $rct.cst));
|
||||||
Array.prototype.push.apply(tda, [
|
Array.prototype.push.apply(tda, [
|
||||||
$$.tdc('txt', { colspan: !co.q ? 2 : 1, title: co.tt }).append(co.t),
|
$$.tdc('txt', { colspan: !co.q ? 2 : 1, title: co.tt }).append(co.t),
|
||||||
$$.tdc('currency').text(fnum(co.v, $rct.cst)),
|
$$.tdc('currency').text(cur(co.v)),
|
||||||
$$.tdc('currency inetval').text(fnum(co.vt, $rct.cst)).attr('title', $rct.svcPart + ': ' + fnum(co.vs, $rct.cst))
|
$$.tdc('currency inetval').text(cur(co.vt)).attr('title', $rct.svcPart + ': ' + cur(co.vs))
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
rw.empty().attr('class', ph ? 'placeholder' : 'itm').aC(co.Typ).tC('hidenote', hn).append(tda);
|
rw.empty().attr('class', ph ? 'placeholder' : 'itm').aC(co.Typ)
|
||||||
|
.tC('sethdr', (co.typ || '').toString().toLowerCase() === 'set') /* emphasise set rows (fn1 header + block set row) */
|
||||||
|
.tC('hidenote', hn).append(tda);
|
||||||
dta.co = co;
|
dta.co = co;
|
||||||
};
|
};
|
||||||
/* "Auf Setpreis umstellen" (see $inv.rrw): sets this Set-header row's price to the sum of
|
/* "Auf Setpreis umstellen" (see $inv.rrw): backend-authoritative (ADR 0006) — the browser only
|
||||||
its member rows' totals (rows in the same block whose [SetItmId] equals this row's id,
|
posts the target set-header item id; the server (InvoiceDraftEditService.ApplyItemSetPrice)
|
||||||
the authoritative grouping from fds__prepInvoice), then clears each member's price so the
|
sums the member rows (same block, [SetItmId] equal to this row's id), writes that sum onto
|
||||||
member items show without a price — matching the SetPrice presentation, but as an actual,
|
the set header and clears each member's price, then the normal draftReady refresh re-renders
|
||||||
irreversible data change instead of a display-mode toggle (INVOICE_SET_PRICING.md's
|
both the header and its members from the authoritative session state. This mirrors the PDF's
|
||||||
setmode remains display-only and is unaffected by this). */
|
read of the same cached session, so the online editor and PDF can never drift apart. */
|
||||||
$inv.toSetPrice = function (ev) {
|
$inv.toSetPrice = function (ev) {
|
||||||
let rw = ev.data, dta = rw.data(), id = (dta.Id || '').toString();
|
let rw = ev.data, dta = rw.data(), id = (dta.Id || '').toString();
|
||||||
if (id === '') { return; }
|
if (id === '') { return; }
|
||||||
if (confirm($rct.toSetPc) === false) { return; }
|
if (confirm($rct.toSetPc) === false) { return; }
|
||||||
let bdy = rw.closest('tbody'), members = bdy.find('tr.itm').filter(function () {
|
/* Wait for the flush of any pending local edits to actually reach the server before sending
|
||||||
return (($(this).data('SetItmId') || '').toString()) === id;
|
the conversion delta — sending both in parallel let a delayed block.replace (still carrying
|
||||||
});
|
the pre-conversion, zero member/header values) land after item.setprice and silently
|
||||||
let sum_net_val = 0, sum_vat_val = 0, sum_svcnet_val = 0, sum_svcvat_val = 0;
|
overwrite the just-converted totals back to 0. */
|
||||||
members.each(function () {
|
$.when($inv.t_fds_inv()).done(() => { $inv.d.sync({ Target: 'item.setprice', Ref: id }); });
|
||||||
let m = $(this).data();
|
|
||||||
sum_net_val += (m.net_val || 0);
|
|
||||||
sum_vat_val += (m.vat_val || 0);
|
|
||||||
sum_svcnet_val += (m.svcnet_val || 0);
|
|
||||||
sum_svcvat_val += (m.svcvat_val || 0);
|
|
||||||
$.extend(m, { net: 0, net_val: 0, vat_val: 0, svcnet_val: 0, svcvat_val: 0, Discount: 0 });
|
|
||||||
$inv.rrw.call($(this));
|
|
||||||
});
|
|
||||||
$.extend(dta, {
|
|
||||||
net: sum_net_val, quantityhours: 1, Discount: 0,
|
|
||||||
net_val: sum_net_val, vat_val: sum_vat_val, svcnet_val: sum_svcnet_val, svcvat_val: sum_svcvat_val
|
|
||||||
});
|
|
||||||
$inv.rrw.call(rw);
|
|
||||||
$inv.t_fds_inv();
|
|
||||||
};
|
};
|
||||||
|
/* No client-side totals/footer/tax computation (ADR 0006/0008): this only re-assembles each
|
||||||
|
block's row contract array (needed to post the "req" shape to the server) and, on the very
|
||||||
|
first pass, seeds the authoritative backend session. Footer, VAT breakdown, service-refund
|
||||||
|
note figures and the per-block sum cell are never computed here — they are rendered exclusively
|
||||||
|
from the server's `dstate` response by $inv.d.footer / $inv.d.applyState once the draft has a
|
||||||
|
token. Before a token exists the footer/notes/isum cells are simply left empty; there is no
|
||||||
|
local approximation to display. */
|
||||||
$inv.invSumUpdate = function () {
|
$inv.invSumUpdate = function () {
|
||||||
let tbl = $(this), ft = tbl.children('tfoot').empty(), p13b = bool((tbl.data().admin || {}).p13b || '', false);
|
let tbl = $(this);
|
||||||
tbl.nextAll('.fnote').remove();
|
let ba = [];
|
||||||
let sms = { ttn: 0, ttb: 0, ttvat: 0, tscn: 0, tscvat: 0, vat: {}, itmnet: {} }, ba = [];
|
|
||||||
let rwcy = (lbl, val, cls) => $$.tdc('currency', $$.tr(ft, { class: cls || 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text(lbl)]), fnum(val, $rct.cst)), fn = (t) => $$.dc('fnote').insertAfter(tbl).rwText(t);
|
|
||||||
let csms = function (rrx, sms, sid) {
|
|
||||||
sms.tscn += (rrx.svcnet_val || 0);
|
|
||||||
sms.tscvat += (rrx.svcvat_val || 0);
|
|
||||||
sms.ttn += (rrx.net_val || 0);
|
|
||||||
sms.ttvat += (rrx.vat_val || 0);
|
|
||||||
sms.ttb += ((rrx.net_val || 0) + (rrx.vat_val || 0));
|
|
||||||
if ((rrx.vat || '') !== '') {
|
|
||||||
sms.vat[rrx.vat] = (sms.vat[rrx.vat] || 0) + (rrx.vat_val || 0);
|
|
||||||
}
|
|
||||||
//sms.itmnet[sid] = (sms.itmnet[sid] || 0) + (rrx.net_val || 0);
|
|
||||||
};
|
|
||||||
let bds = tbl.children('tbody');
|
let bds = tbl.children('tbody');
|
||||||
bds.each((bi, bdy) => {
|
bds.each((bi, bdy) => {
|
||||||
let b = $(bdy), rx = b.data() || {}, i = [], citems = [], cset = null, bnet = 0, itm = b.find('tr.itm'), iso = 0, ipos = 0;
|
let b = $(bdy), rx = b.data() || {}, i = [], citems = [], itm = b.find('tr.itm'), iso = 0, ipos = 0;
|
||||||
b.tC('empty', itm.length < 1);
|
b.tC('empty', itm.length < 1);
|
||||||
itm.each((ti, tx) => {
|
itm.each((ti, tx) => {
|
||||||
|
let rrx = $(tx).data() || {};
|
||||||
let rrx = $(tx).data() || {}; csms(rrx, sms, rx.Id); bnet += (rrx.net_val || 0); i.push(rrx.co);
|
i.push(rrx.co);
|
||||||
//console.debug('rrx %o', rrx);
|
|
||||||
/* backend item contract (title/desc/qty/price_net/total_net + set flags), see InvoiceSetPricing.
|
/* backend item contract (title/desc/qty/price_net/total_net + set flags), see InvoiceSetPricing.
|
||||||
Set grouping: an item of Type 'set' is a header that claims the following items in this
|
Set grouping: fds__prepInvoice computes [SetItmId] per item, anchored on the still-unconverted
|
||||||
block as its members until the next set header (mfr__items has no explicit member link). */
|
(price 0) Set header that owns it — this is the authoritative membership signal (not row order),
|
||||||
|
so items not tagged with a SetItmId by the server are never swept into a preceding set. The
|
||||||
|
header row's own SetItmId self-references its own id, but it is never its own member (excluded
|
||||||
|
by the id !== '' && !== citem.id check below). */
|
||||||
let citem = $inv.itemToContract(rrx);
|
let citem = $inv.itemToContract(rrx);
|
||||||
if (citem.type === 'set' && citem.id !== '') { cset = citem.id; }
|
let sid = (rrx.SetItmId || '').toString();
|
||||||
else if (cset !== null && (citem.id || '') !== '') { citem.setId = cset; }
|
if (citem.type !== 'set' && sid !== '' && sid !== citem.id) { citem.setId = sid; }
|
||||||
citems.push(citem);
|
citems.push(citem);
|
||||||
if (((typeof rrx.SortOrder === 'undefined' || rrx.SortOrder === null) ? -1 : rrx.SortOrder) > -1) {
|
if (((typeof rrx.SortOrder === 'undefined' || rrx.SortOrder === null) ? -1 : rrx.SortOrder) > -1) {
|
||||||
if (['text', 'title'].includes((rrx.Type || 'other').toLowerCase()) === false) { ipos++; }
|
if (['text', 'title'].includes((rrx.Type || 'other').toLowerCase()) === false) { ipos++; }
|
||||||
@@ -1690,41 +1756,10 @@ $inv.invSumUpdate = function () {
|
|||||||
$inv.rrw.call(tx);
|
$inv.rrw.call(tx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
//console.debug('%o', {
|
ba.push({ Id: rx.Id, nme: rx.Name, text: rx.text, itm: i, items: citems });
|
||||||
// f: b.find('tr.isum > td.isumval'), t: fnum(bnet, $rct.cst), n: bnet
|
|
||||||
//});
|
|
||||||
b.find('tr.isum > td.isumval').text(fnum(bnet, $rct.cst));
|
|
||||||
ba.push({ Id: rx.Id, nme: rx.Name, text: rx.text, itm: i, items: citems, netval: bnet });
|
|
||||||
});
|
});
|
||||||
let nonempty = tbl.find('tbody:not(.empty)').length;
|
let nonempty = tbl.find('tbody:not(.empty)').length;
|
||||||
bds.find('tr.isum').tC('hidden', nonempty < 2);
|
bds.find('tr.isum').tC('hidden', nonempty < 2);
|
||||||
//let fnet = $$.tdc('currency', $$.tr(ft, { class: 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text('Netto')]), fnum(sms.ttn, $rct.cst));
|
|
||||||
rwcy('Netto', sms.ttn);
|
|
||||||
if (p13b === false) {
|
|
||||||
$.each(sms.vat, (vi, vx) => {
|
|
||||||
//$$.tdc('currency vat', $$.tr(ft, { class: 'tvat' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text('Umsatzsteuer ' + vi)]), fnum(vx, $rct.cst));
|
|
||||||
rwcy($rct.vat + ' ' + vi, vx, 'tvat');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
sms.ttb = sms.ttn;
|
|
||||||
}
|
|
||||||
//let fsum = $$.tdc('currency', $$.tr(ft, { class: 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text('Summe')]), fnum(sms.ttb, $rct.cst));
|
|
||||||
rwcy('Summe', sms.ttb);
|
|
||||||
let itype = tbl.data().admin.type;
|
|
||||||
if (itype === 'i') {
|
|
||||||
fn($rct.note2);
|
|
||||||
fn($rct.note4);
|
|
||||||
} else if (itype === 'c') {
|
|
||||||
fn($rct.note2);
|
|
||||||
} else {
|
|
||||||
fn(string($rct.note3, [fnum((sms.tscn + sms.tscvat) * (tbl.data().admin.tax_servicerefund || 0), $rct.cst)])).aC('ntax');
|
|
||||||
fn($rct.note2);
|
|
||||||
fn(string($rct.note1, [fnum(sms.tscn + sms.tscvat, $rct.cst), fnum(sms.tscn, $rct.cst), fnum(sms.tscvat, $rct.cst)]));
|
|
||||||
}
|
|
||||||
if (p13b === true) {
|
|
||||||
fn($rct.note13b);
|
|
||||||
}
|
|
||||||
tbl.data('sms', sms);
|
|
||||||
tbl.data('bai', ba);
|
tbl.data('bai', ba);
|
||||||
/* Backend-authoritative seeding (ADR 0006): on the first calculation of an invoice
|
/* Backend-authoritative seeding (ADR 0006): on the first calculation of an invoice
|
||||||
draft (invSumUpdate is only bound for invoices, never reminders), hand the assembled
|
draft (invSumUpdate is only bound for invoices, never reminders), hand the assembled
|
||||||
@@ -1824,8 +1859,16 @@ $inv.t_fds_inv = () => {
|
|||||||
let tbl = $('div.invoice_layout table.invi');
|
let tbl = $('div.invoice_layout table.invi');
|
||||||
tbl.trigger('fds.inv');
|
tbl.trigger('fds.inv');
|
||||||
/* After any local item mutation, push the changed block(s) to the authoritative
|
/* After any local item mutation, push the changed block(s) to the authoritative
|
||||||
server session as granular deltas (invoice drafts only — reminders have no token). */
|
server session as granular deltas (invoice drafts only — reminders have no token,
|
||||||
if ((tbl.data('dtoken') || '') !== '') { $inv.d.syncChanged(tbl); }
|
nor a 'dseedpromise', so they correctly fall through to the no-op). While the initial
|
||||||
|
seed (inv/dopen) is still in flight there is no token yet either, but a 'dseedpromise'
|
||||||
|
is pending — wait for it instead of skipping the flush, otherwise an edit made in that
|
||||||
|
brief window is silently never sent. Returns the flush promise so callers that must
|
||||||
|
sequence a follow-up server delta after this flush (e.g. $inv.toSetPrice) can wait for
|
||||||
|
it instead of racing it. */
|
||||||
|
if ((tbl.data('dtoken') || '') !== '') { return $inv.d.syncChanged(tbl); }
|
||||||
|
let seeding = tbl.data('dseedpromise');
|
||||||
|
return seeding ? $.when(seeding).then(() => $inv.d.syncChanged(tbl)) : $.when();
|
||||||
};
|
};
|
||||||
$inv.sedit = () => {
|
$inv.sedit = () => {
|
||||||
$inv.sprev(true);
|
$inv.sprev(true);
|
||||||
@@ -1892,7 +1935,10 @@ $inv.itemToContract = function (rrx) {
|
|||||||
rrx = rrx || {};
|
rrx = rrx || {};
|
||||||
let oHtml = (e) => $$.d().append(e).html();
|
let oHtml = (e) => $$.d().append(e).html();
|
||||||
let type = (rrx.Type || '').toString().toLowerCase();
|
let type = (rrx.Type || '').toString().toLowerCase();
|
||||||
let ci = { id: (rrx.Id || '').toString(), type: type, title: '', desc: '', qty: '', price_net: '', total_net: (rrx.net_val || 0), vat: rrx.vat || '' };
|
/* ADR 0009: preserve null prices verbatim (null = empty cell, kept distinct from a real 0) so
|
||||||
|
block-mode nulled members and the set row round-trip unchanged when the block is re-posted. */
|
||||||
|
let nz = (x) => (x == null ? null : x);
|
||||||
|
let ci = { id: (rrx.Id || '').toString(), type: type, title: '', desc: '', qty: '', price_net: '', total_net: nz(rrx.net_val), vat: rrx.vat || '' };
|
||||||
if (rrx.co && rrx.co.typ === 'osum') {
|
if (rrx.co && rrx.co.typ === 'osum') {
|
||||||
/* combined single-sum line — the on-screen "title" is an HTML sub-table; render it as desc */
|
/* combined single-sum line — the on-screen "title" is an HTML sub-table; render it as desc */
|
||||||
ci.desc = rrx.co.t || '';
|
ci.desc = rrx.co.t || '';
|
||||||
@@ -1913,30 +1959,35 @@ $inv.itemToContract = function (rrx) {
|
|||||||
ci.desc = rrx.Note || '';
|
ci.desc = rrx.Note || '';
|
||||||
}
|
}
|
||||||
ci.qty = rrx.quantity || ((rrx.quantityhours || 0) !== 0 ? (fnum(rrx.quantityhours) + (rrx.UnitString ? ' ' + rrx.UnitString : '')) : '');
|
ci.qty = rrx.quantity || ((rrx.quantityhours || 0) !== 0 ? (fnum(rrx.quantityhours) + (rrx.UnitString ? ' ' + rrx.UnitString : '')) : '');
|
||||||
ci.price_net = (rrx.net || 0);
|
ci.price_net = nz(rrx.net);
|
||||||
ci.total_net = (rrx.net_val || 0);
|
ci.total_net = nz(rrx.net_val);
|
||||||
}
|
}
|
||||||
return ci;
|
return ci;
|
||||||
};
|
};
|
||||||
/* 3-way set-pricing display switch. Mirrors §13b: writes the choice onto admin.setmode,
|
/* 2-way set-pricing display switch (SetPrice / SetOnly). Mirrors §13b: writes the choice onto
|
||||||
which BuildInvoiceParams turns into the "setmode:<mode>" InvoiceOptions token the PDF reads. */
|
admin.setmode, which BuildInvoiceParams turns into the "setmode:<mode>" InvoiceOptions token
|
||||||
|
the PDF reads. ItemPrices ("Positionen mit Preis – Set als Überschrift") was removed: it was
|
||||||
|
always just the implicit pre-conversion default and is not a state the user can switch back to
|
||||||
|
once a set has been converted via the "Auf Setpreis umstellen" item switch (see $inv.toSetPrice /
|
||||||
|
InvoiceDraftEditService.ApplyItemSetPrice) — that conversion is one-way. */
|
||||||
$inv.ssetmode = () => {
|
$inv.ssetmode = () => {
|
||||||
let l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data();
|
let l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data();
|
||||||
d.admin = d.admin || {};
|
d.admin = d.admin || {};
|
||||||
let cur = (d.admin.setmode || 'setprice'), o;
|
let cur = (d.admin.setmode || 'setprice'), o;
|
||||||
let btn = (mode) => $$.dc('btn', $ict.setmo[mode]).tC('selected', cur === mode).click(() => { o.c.trigger('modal_close'); $inv.setSetmode(mode); });
|
let btn = (mode) => $$.dc('btn', $ict.setmo[mode]).tC('selected', cur === mode).click(() => { o.c.trigger('modal_close'); $inv.setSetmode(mode); });
|
||||||
let fr = $$.dc('choicefrm').append([btn('setprice'), btn('itemprices'), btn('setonly')]);
|
let fr = $$.dc('choicefrm').append([btn('setprice'), btn('setonly')]);
|
||||||
o = $ocms.dlg(fr, { width: 800 });
|
o = $ocms.dlg(fr, { width: 800 });
|
||||||
};
|
};
|
||||||
|
/* "Set mit Preis" / "Nur Set mit Preis" (ADR 0009): NOT a display toggle — an irreversible,
|
||||||
|
backend-authoritative mutation grouped by service-request block. The server inserts a dedicated
|
||||||
|
set row per block (block sum) and either nulls each member's price (setprice) or removes the
|
||||||
|
members (setonly); the normal draftReady refresh re-renders both via $inv.d.applyItems. The
|
||||||
|
browser only posts the chosen mode. Flush pending local edits first so a delayed block.replace
|
||||||
|
can't land after the conversion and overwrite it (same race guard as $inv.toSetPrice). */
|
||||||
$inv.setSetmode = (mode) => {
|
$inv.setSetmode = (mode) => {
|
||||||
let l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data();
|
if (confirm($rct.toSetMc || $rct.toSetPc) === false) { return; }
|
||||||
d.admin = d.admin || {};
|
let target = (mode === 'setonly') ? 'block.setonly' : 'block.setprice';
|
||||||
d.admin.setmode = mode; /* posted in admin -> BuildInvoiceParams writes setmode: into InvoiceOptions */
|
$.when($inv.t_fds_inv()).done(() => { $inv.d.sync({ Target: target }); });
|
||||||
d.inv = d.inv || {}; /* keep a local InvoiceOptions reflection in sync (cosmetic) */
|
|
||||||
let opts = (d.inv.InvoiceOptions || '').split(',').filter(x => x !== '' && x.indexOf('setmode:') !== 0);
|
|
||||||
if (mode && mode !== 'setprice') { opts.push('setmode:' + mode); }
|
|
||||||
d.inv.InvoiceOptions = opts.join(',');
|
|
||||||
$inv.d.sync({ Target: 'setmode', Value: mode });
|
|
||||||
};
|
};
|
||||||
$inv.sctp = () => {
|
$inv.sctp = () => {
|
||||||
let flds = $invcol.ctp;
|
let flds = $invcol.ctp;
|
||||||
@@ -1959,24 +2010,21 @@ $inv.sctp = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
/* Normalises the editor's working model into the exact field names the C# backend
|
/* Normalises the editor's working model into the exact field names the C# backend
|
||||||
(FdsInvoiceData.BuildInvoiceParams) reads, then returns the `invc` payload:
|
(FdsInvoiceData.BuildInvoiceParams) reads, then returns the `invc` payload. No totals/VAT
|
||||||
- balances/service sums come from `sms` (ttn/ttb), exposed on `new` as total_net/total_gross;
|
are computed here (ADR 0006/0008: the online editor performs no arithmetic at all) — the
|
||||||
- every VAT rate's net amount is exposed as new.vat_<rate>_net (the backend reads the highest);
|
backend session computes total_net/total_gross/vat itself (InvoiceDraftCalculator) once
|
||||||
|
opened from this payload, and BuildFdsData/BuildInvoiceParams reads them from there:
|
||||||
- new.invoicetitle -> new.title, new.loc -> new.provisionlocation, admin.paymentterms ->
|
- new.invoicetitle -> new.title, new.loc -> new.provisionlocation, admin.paymentterms ->
|
||||||
new.paymentterm, admin.CustomerId -> admin.customerid.
|
new.paymentterm, admin.CustomerId -> admin.customerid.
|
||||||
Originals are kept alongside; the source objects are not mutated. */
|
Originals are kept alongside; the source objects are not mutated. */
|
||||||
$inv.invcPayload = function (d) {
|
$inv.invcPayload = function (d) {
|
||||||
d = d || {};
|
d = d || {};
|
||||||
let sms = d.sms || {}, nw = $.extend({}, d.new), adm = $.extend({}, d.admin);
|
let nw = $.extend({}, d.new), adm = $.extend({}, d.admin);
|
||||||
nw.total_net = sms.ttn || 0;
|
|
||||||
nw.total_gross = sms.ttb || 0;
|
|
||||||
/* VAT (rate + amount) is taken by the backend straight from the posted sms.vat map
|
|
||||||
(FdsInvoiceData.HighestVat), so no per-rate new.vat_* keys are needed here. */
|
|
||||||
nw.title = (nw.invoicetitle != null ? nw.invoicetitle : (nw.title || ''));
|
nw.title = (nw.invoicetitle != null ? nw.invoicetitle : (nw.title || ''));
|
||||||
nw.provisionlocation = (nw.loc != null ? nw.loc : (nw.provisionlocation || ''));
|
nw.provisionlocation = (nw.loc != null ? nw.loc : (nw.provisionlocation || ''));
|
||||||
nw.paymentterm = (adm.paymentterms != null ? adm.paymentterms : (nw.paymentterm || ''));
|
nw.paymentterm = (adm.paymentterms != null ? adm.paymentterms : (nw.paymentterm || ''));
|
||||||
adm.customerid = (adm.customerid != null ? adm.customerid : adm.CustomerId);
|
adm.customerid = (adm.customerid != null ? adm.customerid : adm.CustomerId);
|
||||||
return { admin: adm, req: d.bai, sms: d.sms, new: nw };
|
return { admin: adm, req: d.bai, new: nw };
|
||||||
};
|
};
|
||||||
/* Zwischenspeichern and preview now run against the backend-authoritative session
|
/* Zwischenspeichern and preview now run against the backend-authoritative session
|
||||||
(ADR 0006): no full-invoice re-upload — the cached draft is flushed / rendered by
|
(ADR 0006): no full-invoice re-upload — the cached draft is flushed / rendered by
|
||||||
@@ -1990,20 +2038,11 @@ $inv.rReload = () => {
|
|||||||
$inv.cInv2({ id: s.search });
|
$inv.cInv2({ id: s.search });
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
};
|
};
|
||||||
$inv.quantChange = function (i) {
|
/* No client-side arithmetic (ADR 0006/0008): quantity/price/VAT edits are posted as raw values
|
||||||
//console.debug({ t: this, i: i });
|
and the server (InvoiceDraftCalculator.RecomputeLineValues) computes net_val/vat_val/
|
||||||
let t = $(this), f = t.closest('form'), fi = {}, pf = (i) => parseFloat(i.toString().replace('%', '').replace(',', '.')), rtp = (num) => num.toFixed(2);
|
svcnet_val/svcvat_val. The fields are left as entered here; the authoritative values are
|
||||||
f.find(':input').each((i, e) => { fi[$(e).attr('name')] = $(e); });
|
pushed back onto the row by $inv.d.applyItems once the change reaches the backend. */
|
||||||
let qv = parseInt(fi.quantityhours.val() || '0'), nv = pf(fi.net.val() || '0'), vat = pf(fi.vat.val()) * 0.01;
|
$inv.quantChange = function (i) { };
|
||||||
if (qv > 0 && nv > 0) {
|
|
||||||
fi.net_val.val(rtp(qv * nv));
|
|
||||||
fi.vat_val.val(rtp(qv * nv * vat));
|
|
||||||
if (['Service'].includes(fi.Type.val())) {
|
|
||||||
fi.svcnet_val.val(rtp(qv * nv));
|
|
||||||
fi.svcvat_val.val(rtp(qv * nv * vat));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
$inv.storno = function (id, fds) {
|
$inv.storno = function (id, fds) {
|
||||||
let o, fr = $$.dc('choicefrm').append([
|
let o, fr = $$.dc('choicefrm').append([
|
||||||
$$.dc('btn', 'Storno ohne Details').click({ id: id, mode: 'simple' }, (ev) => { o.c.trigger('modal_close'); $inv.cSt(ev.data); })
|
$$.dc('btn', 'Storno ohne Details').click({ id: id, mode: 'simple' }, (ev) => { o.c.trigger('modal_close'); $inv.cSt(ev.data); })
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -814,7 +814,7 @@ $ocms.postXT = function (options) {
|
|||||||
if ((typeof options.contentType === 'boolean' ? options.contentType === false : false) === true) {
|
if ((typeof options.contentType === 'boolean' ? options.contentType === false : false) === true) {
|
||||||
ajo.contentType = false; /* needed for file uploads */
|
ajo.contentType = false; /* needed for file uploads */
|
||||||
}
|
}
|
||||||
$.ajax(ajo);
|
return $.ajax(ajo);
|
||||||
};
|
};
|
||||||
$ocms.cex_timer = function () {
|
$ocms.cex_timer = function () {
|
||||||
if (!$ocms.cexi) {
|
if (!$ocms.cexi) {
|
||||||
@@ -2670,7 +2670,15 @@ if (!Element.prototype.closest) {
|
|||||||
_onPress: function (e) {
|
_onPress: function (e) {
|
||||||
if (!e) { return; }
|
if (!e) { return; }
|
||||||
function op(tgt) {
|
function op(tgt) {
|
||||||
if (tgt && tgt.parentNode === this._container && (this._dragHandleClass === '' || e.target.className.indexOf(this._dragHandleClass) > -1) && tgt.className.indexOf("nosort") < 0) {
|
// Match the drag handle by real CSS class token, not substring. The old
|
||||||
|
// e.target.className.indexOf('ico') matched every Bootstrap "glyph[ico]n" icon,
|
||||||
|
// so clicking any icon button (edit/delete/set-price…) inside a sortable row
|
||||||
|
// started a zero-distance drag that swallowed the button's own click. Using
|
||||||
|
// closest('.<handle>') restricts dragging to the dedicated handle element (e.g.
|
||||||
|
// the ".ico" move button) and its descendants only.
|
||||||
|
var isHandle = this._dragHandleClass === '' ||
|
||||||
|
(e.target.closest && e.target.closest('.' + this._dragHandleClass) != null);
|
||||||
|
if (tgt && tgt.parentNode === this._container && isHandle && tgt.className.indexOf("nosort") < 0) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this._dragging = true;
|
this._dragging = true;
|
||||||
this._click = getPoint(e);
|
this._click = getPoint(e);
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+201
-162
@@ -36,6 +36,7 @@ let $rct = {
|
|||||||
combP: 'Positionen zusammenfassen',
|
combP: 'Positionen zusammenfassen',
|
||||||
toSetP: 'Auf Setpreis umstellen (Positionen ohne Preis)',
|
toSetP: 'Auf Setpreis umstellen (Positionen ohne Preis)',
|
||||||
toSetPc: 'Der Set-Preis wird auf die Summe seiner Positionen gesetzt und die Positionen selbst verlieren ihren Preis. Das ist nicht rückgängig zu machen (der Betrag kann danach nur noch manuell geändert werden). Fortfahren?',
|
toSetPc: 'Der Set-Preis wird auf die Summe seiner Positionen gesetzt und die Positionen selbst verlieren ihren Preis. Das ist nicht rückgängig zu machen (der Betrag kann danach nur noch manuell geändert werden). Fortfahren?',
|
||||||
|
toSetMc: 'Jeder Leistungsauftrag wird zu einer Set-Position mit der Summe als Preis zusammengefasst. Das ist nicht rückgängig zu machen (die Set-Preise können danach nur noch manuell geändert werden). Fortfahren?',
|
||||||
iSum: 'Zwischensumme',
|
iSum: 'Zwischensumme',
|
||||||
dtRel: 'Freigegeben am: ',
|
dtRel: 'Freigegeben am: ',
|
||||||
dtCr: 'Erstellt am: ',
|
dtCr: 'Erstellt am: ',
|
||||||
@@ -151,7 +152,6 @@ let $ict = {
|
|||||||
setm: 'Set-Preisanzeige',
|
setm: 'Set-Preisanzeige',
|
||||||
setmo: {
|
setmo: {
|
||||||
setprice: 'Set mit Preis – Positionen ohne Preis',
|
setprice: 'Set mit Preis – Positionen ohne Preis',
|
||||||
itemprices: 'Positionen mit Preis – Set als Überschrift',
|
|
||||||
setonly: 'Nur Set mit Preis – Positionen ausgeblendet'
|
setonly: 'Nur Set mit Preis – Positionen ausgeblendet'
|
||||||
},
|
},
|
||||||
ctp: 'Ansprechpartner festlegen',
|
ctp: 'Ansprechpartner festlegen',
|
||||||
@@ -654,10 +654,13 @@ $inv.d = {
|
|||||||
return h;
|
return h;
|
||||||
},
|
},
|
||||||
/* Seed the authoritative server session from the assembled editor payload. Called
|
/* Seed the authoritative server session from the assembled editor payload. Called
|
||||||
once from invSumUpdate on open; thereafter the backend is the source of truth. */
|
once from invSumUpdate on open; thereafter the backend is the source of truth.
|
||||||
|
The in-flight ajax promise is stashed on 'dseedpromise' so a delta sent (e.g. via
|
||||||
|
a context-menu action) before seeding has actually completed can wait for the token
|
||||||
|
instead of silently no-op'ing (see $inv.d.sync). */
|
||||||
seed: function (payload) {
|
seed: function (payload) {
|
||||||
let l = $inv.d.layout(); l.aC('freeze');
|
let l = $inv.d.layout(); l.aC('freeze');
|
||||||
$ocms.postXT({
|
let p = $ocms.postXT({
|
||||||
url: $ocms.url('inv/dopen'), data: { payload: JSON.stringify(payload) }, success: (r) => {
|
url: $ocms.url('inv/dopen'), data: { payload: JSON.stringify(payload) }, success: (r) => {
|
||||||
$inv.d.tbl().data('dtoken', r.token).data('dver', r.version).data('dhashes', $inv.d.hashes()).data('dorder', $inv.d.order());
|
$inv.d.tbl().data('dtoken', r.token).data('dver', r.version).data('dhashes', $inv.d.hashes()).data('dorder', $inv.d.order());
|
||||||
$fis.draft.bind(r.token, {
|
$fis.draft.bind(r.token, {
|
||||||
@@ -666,8 +669,10 @@ $inv.d = {
|
|||||||
onClosed: (reason) => $inv.d.closed(reason)
|
onClosed: (reason) => $inv.d.closed(reason)
|
||||||
});
|
});
|
||||||
$inv.d.refresh();
|
$inv.d.refresh();
|
||||||
}, error: () => { l.rC('freeze'); }, complete: () => { $inv.d.tbl().removeData('dseeding'); }
|
}, error: () => { l.rC('freeze'); }, complete: () => { $inv.d.tbl().removeData('dseeding').removeData('dseedpromise'); }
|
||||||
});
|
});
|
||||||
|
$inv.d.tbl().data('dseedpromise', p);
|
||||||
|
return p;
|
||||||
},
|
},
|
||||||
/* Re-fetch the authoritative state and render the totals footer + validation from it. */
|
/* Re-fetch the authoritative state and render the totals footer + validation from it. */
|
||||||
refresh: function (cb) {
|
refresh: function (cb) {
|
||||||
@@ -681,11 +686,21 @@ $inv.d = {
|
|||||||
},
|
},
|
||||||
applyState: function (state) {
|
applyState: function (state) {
|
||||||
let tbl = $inv.d.tbl(); if (tbl.length < 1) { return; }
|
let tbl = $inv.d.tbl(); if (tbl.length < 1) { return; }
|
||||||
|
/* Concurrent refreshes (one per flushed block plus one for a follow-up delta such as
|
||||||
|
item.setprice, and SignalR draftReady on top) can have their dstate responses arrive
|
||||||
|
out of order. Applying a response older than the version already rendered would
|
||||||
|
silently roll back a just-applied server-side mutation (e.g. a set-price conversion
|
||||||
|
reverting to its pre-conversion 0/individually-priced state) — so stale responses are
|
||||||
|
dropped here instead of rendered. */
|
||||||
|
let curVer = tbl.data('dver');
|
||||||
|
if (typeof curVer === 'number' && typeof state.version === 'number' && state.version < curVer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
tbl.data('dver', state.version).data('serverSums', state.sums);
|
tbl.data('dver', state.version).data('serverSums', state.sums);
|
||||||
$inv.d.footer(tbl, state.sums || {}, state.admin || {});
|
$inv.d.footer(tbl, state.sums || {}, state.admin || {});
|
||||||
$inv.d.notes(state.notes || []);
|
$inv.d.notes(state.notes || []);
|
||||||
$inv.d.validation(state.validation || []);
|
$inv.d.validation(state.validation || []);
|
||||||
$inv.d.applyPositions(tbl, state.req || []);
|
$inv.d.applyItems(tbl, state.req || []);
|
||||||
$inv.d.applySetDisplay(tbl, state.setDisplay || {});
|
$inv.d.applySetDisplay(tbl, state.setDisplay || {});
|
||||||
/* Keep the menu's "Set-Preisanzeige" entry in sync with the authoritative admin.setmode:
|
/* Keep the menu's "Set-Preisanzeige" entry in sync with the authoritative admin.setmode:
|
||||||
hides it right after a mode switch, and restores it if a discard reverted the draft to
|
hides it right after a mode switch, and restores it if a discard reverted the draft to
|
||||||
@@ -705,19 +720,60 @@ $inv.d = {
|
|||||||
/* Push the server's authoritative position numbers back onto the rendered rows so the online
|
/* Push the server's authoritative position numbers back onto the rendered rows so the online
|
||||||
editor and the PDF preview always agree (the server numbers priced lines continuously; the
|
editor and the PDF preview always agree (the server numbers priced lines continuously; the
|
||||||
browser must not keep its own numbering). Only the position cell is touched — no re-render. */
|
browser must not keep its own numbering). Only the position cell is touched — no re-render. */
|
||||||
applyPositions: function (tbl, req) {
|
/* Reconciles each block's rendered rows against the authoritative server line list
|
||||||
(req || []).forEach((b) => (b && b.itm || []).forEach((co) => {
|
(`req[].itm`, the co shape). The server may not only change a line's values but also
|
||||||
if (!co || (co.id || '') === '') { return; }
|
ADD lines (the block set row inserted by "Set mit Preis"/block.setprice) and REMOVE lines
|
||||||
let cell = tbl.find('#itm' + co.id + ' td.keep').first();
|
("Nur Set mit Preis"/block.setonly), and reorder them — the browser never computes any of
|
||||||
if (cell.length) { cell.text(co.p != null ? co.p : ''); }
|
that (ADR 0006/0008/0009), it just mirrors the server. For each server line: patch/insert
|
||||||
}));
|
the matching row, preserving null price fields verbatim (ADR 0009: null = empty cell, kept
|
||||||
|
distinct from 0). Rows the server no longer lists are removed; order follows the server.
|
||||||
|
Position numbers, net/VAT and the set-row emphasis all come from rrw off the mirrored data. */
|
||||||
|
applyItems: function (tbl, req) {
|
||||||
|
let val = (x) => (x === undefined ? null : x); // JSON null stays null; absent -> null
|
||||||
|
(req || []).forEach((b) => {
|
||||||
|
if (!b) { return; }
|
||||||
|
let bid = (b.Id || '').toString(); if (bid === '') { return; }
|
||||||
|
let bdy = tbl.children('tbody').filter((i, e) => (($(e).data() || {}).Id || '').toString() === bid).first();
|
||||||
|
if (bdy.length < 1) { return; }
|
||||||
|
let itms = b.itm || [], serverIds = {};
|
||||||
|
let title = bdy.find('tr.title').first();
|
||||||
|
let prev = title.length ? title : null;
|
||||||
|
itms.forEach((co) => {
|
||||||
|
let id = (co.id || '').toString(); if (id === '') { return; }
|
||||||
|
serverIds[id] = true;
|
||||||
|
let rw = bdy.find('#itm' + id).first(), isNew = rw.length < 1;
|
||||||
|
if (isNew) { rw = $$.tr(bdy, { id: 'itm' + id, class: 'itm' }); }
|
||||||
|
let dta = rw.data() || {};
|
||||||
|
let changed = isNew || (dta.Type || '') !== (co.typ || '')
|
||||||
|
|| dta.net !== val(co.v) || dta.net_val !== val(co.vt) || dta.vat_val !== val(co.vv)
|
||||||
|
|| dta.svcnet_val !== val(co.vs) || dta.svcvat_val !== val(co.vsv)
|
||||||
|
|| (dta.position || '') !== (co.p != null ? co.p : '');
|
||||||
|
$.extend(dta, {
|
||||||
|
Id: id, Type: co.typ,
|
||||||
|
net: val(co.v), net_val: val(co.vt), vat_val: val(co.vv),
|
||||||
|
svcnet_val: val(co.vs), svcvat_val: val(co.vsv),
|
||||||
|
vat: co.vat, position: (co.p != null ? co.p : ''),
|
||||||
|
SetItmId: (co.SetItmId != null ? co.SetItmId : null)
|
||||||
|
});
|
||||||
|
if (isNew) { $.extend(dta, { htmltext: co.t, quantity: co.q }); } // text only for server-created rows (e.g. the set row)
|
||||||
|
rw.data(dta);
|
||||||
|
if (prev) { if (rw.prev()[0] !== prev[0]) { rw.insertAfter(prev); } } // keep server order, but don't churn already-ordered rows
|
||||||
|
else if (bdy.children().first()[0] !== rw[0]) { bdy.prepend(rw); }
|
||||||
|
prev = rw;
|
||||||
|
if (changed) { $inv.rrw.call(rw); }
|
||||||
|
});
|
||||||
|
bdy.find('tr.itm').each((i, tr) => {
|
||||||
|
let id = (($(tr).data() || {}).Id || '').toString();
|
||||||
|
if (id !== '' && !serverIds[id]) { $(tr).remove(); } // dropped server-side (block.setonly)
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
/* Applies the backend-authoritative set-pricing display flags (see BuildSetDisplay /
|
/* Applies the backend-authoritative set-pricing display flags (see BuildSetDisplay /
|
||||||
InvoiceSetPricing) onto the rendered rows: blanks the price/total cells for lines the
|
InvoiceSetPricing) onto the rendered rows: blanks the price/total cells for lines the
|
||||||
server says should show no price (set members in SetPrice mode, the set header in
|
server says should show no price (set members in SetPrice/SetOnly mode), and emphasises
|
||||||
ItemPrices mode), and emphasises the set header line — so the online editor always
|
the set header line — so the online editor always shows exactly what the PDF will print,
|
||||||
shows exactly what the PDF will print, without duplicating the pricing rules client-side.
|
without duplicating the pricing rules client-side. Rows not present in the map (no set in
|
||||||
Rows not present in the map (no set in that block, or standalone items) are left untouched. */
|
that block, or standalone items) are left untouched. */
|
||||||
applySetDisplay: function (tbl, setDisplay) {
|
applySetDisplay: function (tbl, setDisplay) {
|
||||||
tbl.find('tr.itm').each((i, tr) => {
|
tbl.find('tr.itm').each((i, tr) => {
|
||||||
let rw = $(tr), dta = rw.data() || {}, id = (dta.Id || '').toString();
|
let rw = $(tr), dta = rw.data() || {}, id = (dta.Id || '').toString();
|
||||||
@@ -729,11 +785,23 @@ $inv.d = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
/* Send one change to the server; the draftReady signal and this success both refresh. */
|
/* Send one change to the server; the draftReady signal and this success both refresh.
|
||||||
|
Returns the underlying ajax promise so callers that must not race a follow-up delta
|
||||||
|
against this one (e.g. $inv.toSetPrice after a flush) can chain on it.
|
||||||
|
If the initial seeding (inv/dopen) is still in flight (no token yet, but a
|
||||||
|
'dseedpromise' is pending), the delta previously vanished silently — the user saw no
|
||||||
|
network activity at all when clicking a context-menu action right after opening the
|
||||||
|
editor. Now the delta waits for the seed to finish and retries against the freshly
|
||||||
|
issued token instead of being dropped. */
|
||||||
sync: function (delta) {
|
sync: function (delta) {
|
||||||
let t = $inv.d.token(); if (t === '') { return; }
|
let t = $inv.d.token();
|
||||||
|
if (t === '') {
|
||||||
|
let seeding = $inv.d.tbl().data('dseedpromise');
|
||||||
|
if (seeding) { return $.when(seeding).then(() => $inv.d.sync(delta)); }
|
||||||
|
return $.when();
|
||||||
|
}
|
||||||
$inv.d.layout().aC('freeze');
|
$inv.d.layout().aC('freeze');
|
||||||
$ocms.postXT({
|
return $ocms.postXT({
|
||||||
url: $ocms.url('inv/dpatch'), data: { token: t, delta: JSON.stringify(delta) },
|
url: $ocms.url('inv/dpatch'), data: { token: t, delta: JSON.stringify(delta) },
|
||||||
success: () => { $inv.d.refresh(); },
|
success: () => { $inv.d.refresh(); },
|
||||||
error: (xhr) => { $inv.d.layout().rC('freeze'); if (xhr && xhr.status === 410) { $inv.d.closed('expired'); } }
|
error: (xhr) => { $inv.d.layout().rC('freeze'); if (xhr && xhr.status === 410) { $inv.d.closed('expired'); } }
|
||||||
@@ -744,22 +812,29 @@ $inv.d = {
|
|||||||
/* Diff the freshly-rebuilt blocks against the last-synced state and send only the
|
/* Diff the freshly-rebuilt blocks against the last-synced state and send only the
|
||||||
changed/removed blocks as granular block.replace / block.remove deltas. A pure section
|
changed/removed blocks as granular block.replace / block.remove deltas. A pure section
|
||||||
reorder (same blocks, new sequence) changes no block hash, so it is sent separately as a
|
reorder (same blocks, new sequence) changes no block hash, so it is sent separately as a
|
||||||
block.order delta; the server reorders the cache, renumbers positions and pushes them back. */
|
block.order delta; the server reorders the cache, renumbers positions and pushes them back.
|
||||||
|
No early return on an empty token here: sync() itself waits for an in-flight seed
|
||||||
|
(dseedpromise) instead of silently dropping the delta, so edits made in the brief window
|
||||||
|
before seeding completes are not lost. */
|
||||||
syncChanged: function (tbl) {
|
syncChanged: function (tbl) {
|
||||||
if (($inv.d.token()) === '') { return; }
|
let bai = tbl.data('bai') || [], prev = tbl.data('dhashes') || {}, next = {}, changed = [], removed = [], pending = [];
|
||||||
let bai = tbl.data('bai') || [], prev = tbl.data('dhashes') || {}, next = {}, changed = [], removed = [];
|
|
||||||
$.each(bai, (i, b) => { let id = (b.Id || '').toString(), h = JSON.stringify(b); next[id] = h; if (prev[id] !== h) { changed.push(b); } });
|
$.each(bai, (i, b) => { let id = (b.Id || '').toString(), h = JSON.stringify(b); next[id] = h; if (prev[id] !== h) { changed.push(b); } });
|
||||||
$.each(prev, (id) => { if (next[id] === undefined) { removed.push(id); } });
|
$.each(prev, (id) => { if (next[id] === undefined) { removed.push(id); } });
|
||||||
let order = $inv.d.order(), prevOrder = tbl.data('dorder') || [];
|
let order = $inv.d.order(), prevOrder = tbl.data('dorder') || [];
|
||||||
tbl.data('dhashes', next).data('dorder', order);
|
tbl.data('dhashes', next).data('dorder', order);
|
||||||
changed.forEach((b) => $inv.d.sync({ Target: 'block.replace', Ref: (b.Id || '').toString(), Value: b }));
|
changed.forEach((b) => pending.push($inv.d.sync({ Target: 'block.replace', Ref: (b.Id || '').toString(), Value: b })));
|
||||||
removed.forEach((id) => $inv.d.sync({ Target: 'block.remove', Ref: id }));
|
removed.forEach((id) => pending.push($inv.d.sync({ Target: 'block.remove', Ref: id })));
|
||||||
let sameSet = prevOrder.length === order.length && prevOrder.slice().sort().join(',') === order.slice().sort().join(',');
|
let sameSet = prevOrder.length === order.length && prevOrder.slice().sort().join(',') === order.slice().sort().join(',');
|
||||||
if (sameSet && prevOrder.join(',') !== order.join(',')) { $inv.d.sync({ Target: 'block.order', Value: order }); }
|
if (sameSet && prevOrder.join(',') !== order.join(',')) { pending.push($inv.d.sync({ Target: 'block.order', Value: order })); }
|
||||||
|
/* $.when(...pending) so a caller (e.g. toSetPrice) can wait until every flushed block
|
||||||
|
has actually reached the server before sending a further delta that depends on it —
|
||||||
|
otherwise a delayed block.replace carrying pre-conversion values can land after and
|
||||||
|
silently overwrite a just-applied server-side mutation (e.g. item.setprice). */
|
||||||
|
return $.when.apply($, pending).promise();
|
||||||
},
|
},
|
||||||
/* Map an inline recipient field to its delta target and send it. */
|
/* Map an inline recipient field to its delta target and send it. No token-guard here for the
|
||||||
|
same reason as syncChanged: sync() waits for an in-flight seed instead of dropping the edit. */
|
||||||
syncField: function (nme, val) {
|
syncField: function (nme, val) {
|
||||||
if ($inv.d.token() === '') { return; }
|
|
||||||
let map = { invoicetitle: 'title', invoiceaddress: 'address', invoiceemail: 'email', loc: 'provisionlocation', provisionlocation: 'provisionlocation', provisionperiod: 'provisionperiod' };
|
let map = { invoicetitle: 'title', invoiceaddress: 'address', invoiceemail: 'email', loc: 'provisionlocation', provisionlocation: 'provisionlocation', provisionperiod: 'provisionperiod' };
|
||||||
let target = map[nme]; if (!target) { return; }
|
let target = map[nme]; if (!target) { return; }
|
||||||
$inv.d.sync({ Target: target, Value: val });
|
$inv.d.sync({ Target: target, Value: val });
|
||||||
@@ -914,6 +989,12 @@ $inv.rd = {
|
|||||||
},
|
},
|
||||||
applyState: function (state) {
|
applyState: function (state) {
|
||||||
let tbl = $inv.rd.tbl(); if (tbl.length < 1) { return; }
|
let tbl = $inv.rd.tbl(); if (tbl.length < 1) { return; }
|
||||||
|
/* See $inv.d.applyState — drop stale/out-of-order dstate responses so an older refresh
|
||||||
|
can never roll back a newer one's totals/validation. */
|
||||||
|
let curVer = tbl.data('rdver');
|
||||||
|
if (typeof curVer === 'number' && typeof state.version === 'number' && state.version < curVer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
tbl.data('rdver', state.version).data('serverSums', state.sums).data('remid', state.remid || '');
|
tbl.data('rdver', state.version).data('serverSums', state.sums).data('remid', state.remid || '');
|
||||||
$inv.rd.footer(tbl, state.sums || {});
|
$inv.rd.footer(tbl, state.sums || {});
|
||||||
$inv.rd.notes(state.notes || {});
|
$inv.rd.notes(state.notes || {});
|
||||||
@@ -1473,6 +1554,8 @@ $inv.eHtml = function (ev) {
|
|||||||
}
|
}
|
||||||
$ocms.dlgform(flds, sets);
|
$ocms.dlgform(flds, sets);
|
||||||
};
|
};
|
||||||
|
/* No client-side arithmetic (ADR 0006/0008): only the chosen VAT rate is stored on each row;
|
||||||
|
the server (InvoiceDraftCalculator.RecomputeLineValues) recomputes vat_val/svcvat_val from it. */
|
||||||
$inv.setVat = function (ev) {
|
$inv.setVat = function (ev) {
|
||||||
let t = $(this), thisrow = ev.data;
|
let t = $(this), thisrow = ev.data;
|
||||||
let vat = prompt($rct.rqV);
|
let vat = prompt($rct.rqV);
|
||||||
@@ -1485,12 +1568,6 @@ $inv.setVat = function (ev) {
|
|||||||
thisrow.siblings('.itm').each(function () {
|
thisrow.siblings('.itm').each(function () {
|
||||||
let rwi = $(this), dta = rwi.data();
|
let rwi = $(this), dta = rwi.data();
|
||||||
dta.vat = fnum(vat, { style: 'percent' }).replace(' ', '');
|
dta.vat = fnum(vat, { style: 'percent' }).replace(' ', '');
|
||||||
if ((dta.net_val || 0) > 0) {
|
|
||||||
dta.vat_val = dta.net_val * vat;
|
|
||||||
}
|
|
||||||
if ((dta.svcnet_val || 0) > 0) {
|
|
||||||
dta.svcvat_val = dta.svcnet_val * vat;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
$inv.t_fds_inv();
|
$inv.t_fds_inv();
|
||||||
}
|
}
|
||||||
@@ -1550,12 +1627,14 @@ $inv.rrw = function () {
|
|||||||
if (ph === false) {
|
if (ph === false) {
|
||||||
bc.unshift($$.dc('ibtn edit', { title: $rct.cP }).append(gi('pencil')).click(rw, $inv.eRow));
|
bc.unshift($$.dc('ibtn edit', { title: $rct.cP }).append(gi('pencil')).click(rw, $inv.eRow));
|
||||||
bc.push($$.dc('ibtn del', { title: $rct.dR }).append(gi('trash')).click(function (e) { if (confirm($rct.cD)) { rw.remove(); $inv.t_fds_inv(); } }));
|
bc.push($$.dc('ibtn del', { title: $rct.dR }).append(gi('trash')).click(function (e) { if (confirm($rct.cD)) { rw.remove(); $inv.t_fds_inv(); } }));
|
||||||
/* "Auf Setpreis umstellen": only offered on a Set header item that is not yet
|
/* "Auf Setpreis umstellen": offered on a Set header item that does not yet carry a
|
||||||
priced from its members (fds__prepInvoice's [SetItmId] is null exactly for a
|
price. fds__prepInvoice's [SetItmId] now anchors on the still-unconverted (price 0)
|
||||||
Set header — either already set-priced, or not yet converted). Irreversible:
|
header, so the header row's own SetItmId self-references its own id rather than being
|
||||||
sets this row's price to the sum of its members (rows whose [SetItmId] equals
|
null — the real signal for showing this button is still the header's own price/net_val
|
||||||
this row's id) and clears each member's price. */
|
being 0 (once converted it carries a nonzero price and the button disappears).
|
||||||
if ((dta.Type || '').toLowerCase() === 'set' && (dta.SetItmId || '') === '') {
|
Irreversible: the actual sum/clear mutation happens server-side (backend-authoritative,
|
||||||
|
see InvoiceDraftEditService.ApplyItemSetPrice) via the "item.setprice" patch target. */
|
||||||
|
if ((dta.Type || '').toLowerCase() === 'set' && (dta.net_val || 0) === 0) {
|
||||||
bc.push($$.dc('ibtn tosetp', { title: $rct.toSetP }).append(gi('scale')).click(rw, $inv.toSetPrice));
|
bc.push($$.dc('ibtn tosetp', { title: $rct.toSetP }).append(gi('scale')).click(rw, $inv.toSetPrice));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1565,7 +1644,7 @@ $inv.rrw = function () {
|
|||||||
} else if (rw.is('.itm.osum') === true) {
|
} else if (rw.is('.itm.osum') === true) {
|
||||||
co = { invrqid: dta.InvRqId, id: 'osum' + rw.index(), typ: 'osum', p: '', q: null, t: oHtml(dta.tbl.tbl), tt: null, v: null, vt: dta.net_val, vs: dta.svcnet_val, vat: dta.vat, vv: dta.vat_val, vsv: dta.svcvat_val, det: false };
|
co = { invrqid: dta.InvRqId, id: 'osum' + rw.index(), typ: 'osum', p: '', q: null, t: oHtml(dta.tbl.tbl), tt: null, v: null, vt: dta.net_val, vs: dta.svcnet_val, vat: dta.vat, vv: dta.vat_val, vsv: dta.svcvat_val, det: false };
|
||||||
} else {
|
} else {
|
||||||
co = { invrqid: dta.InvRqId, id: dta.Id || '', typ: dta.Type || 'other', p: '', q: null, t: '', tt: null, v: null, vt: dta.net_val, vs: dta.svcnet_val, vat: dta.vat, vv: dta.vat_val, vsv: dta.svcvat_val, det: (dta.Note || '') !== '' && hn === false };
|
co = { invrqid: dta.InvRqId, id: dta.Id || '', typ: dta.Type || 'other', p: '', q: null, t: '', tt: null, v: null, vt: dta.net_val, vs: dta.svcnet_val, vat: dta.vat, vv: dta.vat_val, vsv: dta.svcvat_val, det: (dta.Note || '') !== '' && hn === false, SetItmId: dta.SetItmId || null };
|
||||||
$$.dc('ibtn ico move', axf, { title: $rct.mR }); /* should be the last added */
|
$$.dc('ibtn ico move', axf, { title: $rct.mR }); /* should be the last added */
|
||||||
|
|
||||||
co.p = dta.position || (dta.SortOrder || '');
|
co.p = dta.position || (dta.SortOrder || '');
|
||||||
@@ -1575,7 +1654,7 @@ $inv.rrw = function () {
|
|||||||
co.t = dta.htmltext || (((dta.NameOrNumber || '').substr(0, 1) !== '#' ? oHtml($$[0]('p').text(dta.NameOrNumber)) : '') + (dta.Note || ''));
|
co.t = dta.htmltext || (((dta.NameOrNumber || '').substr(0, 1) !== '#' ? oHtml($$[0]('p').text(dta.NameOrNumber)) : '') + (dta.Note || ''));
|
||||||
} else {
|
} else {
|
||||||
co.tt = co.det ? '' : $$.s(dta.Note || '').text();
|
co.tt = co.det ? '' : $$.s(dta.Note || '').text();
|
||||||
co.q = dta.quantity || (fnum(dta.quantityhours) + ' ' + (dta.UnitString || ''));
|
co.q = dta.quantity || ((dta.quantityhours || 0) !== 0 ? (fnum(dta.quantityhours) + ' ' + (dta.UnitString || '')) : ''); /* guard: no quantityhours -> blank, not fnum(undefined)="NaN" (e.g. the block set row) */
|
||||||
co.t = dta.htmltext ||(co.det ? (oHtml($$.s(dta.NameOrNumber || '')) + oHtml($$.dc('desc').html(dta.Note))) : oHtml($$.s(dta.NameOrNumber || '')));
|
co.t = dta.htmltext ||(co.det ? (oHtml($$.s(dta.NameOrNumber || '')) + oHtml($$.dc('desc').html(dta.Note))) : oHtml($$.s(dta.NameOrNumber || '')));
|
||||||
co.v = dta.net
|
co.v = dta.net
|
||||||
co.vt = dta.net_val
|
co.vt = dta.net_val
|
||||||
@@ -1594,75 +1673,62 @@ $inv.rrw = function () {
|
|||||||
tda.push($$.td(rw, { colspan: 4 }).append(co.t));
|
tda.push($$.td(rw, { colspan: 4 }).append(co.t));
|
||||||
} else {
|
} else {
|
||||||
Array.prototype.push.apply(tda, co.q ? [$$.tdc('keep').text(co.q)] : []);
|
Array.prototype.push.apply(tda, co.q ? [$$.tdc('keep').text(co.q)] : []);
|
||||||
|
/* ADR 0009: a null/undefined price renders an EMPTY cell (block-mode "Set mit Preis"
|
||||||
|
members, and the set row's own unit-price), distinct from a real 0,00 €. */
|
||||||
|
let cur = (x) => (x == null ? '' : fnum(x, $rct.cst));
|
||||||
Array.prototype.push.apply(tda, [
|
Array.prototype.push.apply(tda, [
|
||||||
$$.tdc('txt', { colspan: !co.q ? 2 : 1, title: co.tt }).append(co.t),
|
$$.tdc('txt', { colspan: !co.q ? 2 : 1, title: co.tt }).append(co.t),
|
||||||
$$.tdc('currency').text(fnum(co.v, $rct.cst)),
|
$$.tdc('currency').text(cur(co.v)),
|
||||||
$$.tdc('currency inetval').text(fnum(co.vt, $rct.cst)).attr('title', $rct.svcPart + ': ' + fnum(co.vs, $rct.cst))
|
$$.tdc('currency inetval').text(cur(co.vt)).attr('title', $rct.svcPart + ': ' + cur(co.vs))
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
rw.empty().attr('class', ph ? 'placeholder' : 'itm').aC(co.Typ).tC('hidenote', hn).append(tda);
|
rw.empty().attr('class', ph ? 'placeholder' : 'itm').aC(co.Typ)
|
||||||
|
.tC('sethdr', (co.typ || '').toString().toLowerCase() === 'set') /* emphasise set rows (fn1 header + block set row) */
|
||||||
|
.tC('hidenote', hn).append(tda);
|
||||||
dta.co = co;
|
dta.co = co;
|
||||||
};
|
};
|
||||||
/* "Auf Setpreis umstellen" (see $inv.rrw): sets this Set-header row's price to the sum of
|
/* "Auf Setpreis umstellen" (see $inv.rrw): backend-authoritative (ADR 0006) — the browser only
|
||||||
its member rows' totals (rows in the same block whose [SetItmId] equals this row's id,
|
posts the target set-header item id; the server (InvoiceDraftEditService.ApplyItemSetPrice)
|
||||||
the authoritative grouping from fds__prepInvoice), then clears each member's price so the
|
sums the member rows (same block, [SetItmId] equal to this row's id), writes that sum onto
|
||||||
member items show without a price — matching the SetPrice presentation, but as an actual,
|
the set header and clears each member's price, then the normal draftReady refresh re-renders
|
||||||
irreversible data change instead of a display-mode toggle (INVOICE_SET_PRICING.md's
|
both the header and its members from the authoritative session state. This mirrors the PDF's
|
||||||
setmode remains display-only and is unaffected by this). */
|
read of the same cached session, so the online editor and PDF can never drift apart. */
|
||||||
$inv.toSetPrice = function (ev) {
|
$inv.toSetPrice = function (ev) {
|
||||||
let rw = ev.data, dta = rw.data(), id = (dta.Id || '').toString();
|
let rw = ev.data, dta = rw.data(), id = (dta.Id || '').toString();
|
||||||
if (id === '') { return; }
|
if (id === '') { return; }
|
||||||
if (confirm($rct.toSetPc) === false) { return; }
|
if (confirm($rct.toSetPc) === false) { return; }
|
||||||
let bdy = rw.closest('tbody'), members = bdy.find('tr.itm').filter(function () {
|
/* Wait for the flush of any pending local edits to actually reach the server before sending
|
||||||
return (($(this).data('SetItmId') || '').toString()) === id;
|
the conversion delta — sending both in parallel let a delayed block.replace (still carrying
|
||||||
});
|
the pre-conversion, zero member/header values) land after item.setprice and silently
|
||||||
let sum_net_val = 0, sum_vat_val = 0, sum_svcnet_val = 0, sum_svcvat_val = 0;
|
overwrite the just-converted totals back to 0. */
|
||||||
members.each(function () {
|
$.when($inv.t_fds_inv()).done(() => { $inv.d.sync({ Target: 'item.setprice', Ref: id }); });
|
||||||
let m = $(this).data();
|
|
||||||
sum_net_val += (m.net_val || 0);
|
|
||||||
sum_vat_val += (m.vat_val || 0);
|
|
||||||
sum_svcnet_val += (m.svcnet_val || 0);
|
|
||||||
sum_svcvat_val += (m.svcvat_val || 0);
|
|
||||||
$.extend(m, { net: 0, net_val: 0, vat_val: 0, svcnet_val: 0, svcvat_val: 0, Discount: 0 });
|
|
||||||
$inv.rrw.call($(this));
|
|
||||||
});
|
|
||||||
$.extend(dta, {
|
|
||||||
net: sum_net_val, quantityhours: 1, Discount: 0,
|
|
||||||
net_val: sum_net_val, vat_val: sum_vat_val, svcnet_val: sum_svcnet_val, svcvat_val: sum_svcvat_val
|
|
||||||
});
|
|
||||||
$inv.rrw.call(rw);
|
|
||||||
$inv.t_fds_inv();
|
|
||||||
};
|
};
|
||||||
|
/* No client-side totals/footer/tax computation (ADR 0006/0008): this only re-assembles each
|
||||||
|
block's row contract array (needed to post the "req" shape to the server) and, on the very
|
||||||
|
first pass, seeds the authoritative backend session. Footer, VAT breakdown, service-refund
|
||||||
|
note figures and the per-block sum cell are never computed here — they are rendered exclusively
|
||||||
|
from the server's `dstate` response by $inv.d.footer / $inv.d.applyState once the draft has a
|
||||||
|
token. Before a token exists the footer/notes/isum cells are simply left empty; there is no
|
||||||
|
local approximation to display. */
|
||||||
$inv.invSumUpdate = function () {
|
$inv.invSumUpdate = function () {
|
||||||
let tbl = $(this), ft = tbl.children('tfoot').empty(), p13b = bool((tbl.data().admin || {}).p13b || '', false);
|
let tbl = $(this);
|
||||||
tbl.nextAll('.fnote').remove();
|
let ba = [];
|
||||||
let sms = { ttn: 0, ttb: 0, ttvat: 0, tscn: 0, tscvat: 0, vat: {}, itmnet: {} }, ba = [];
|
|
||||||
let rwcy = (lbl, val, cls) => $$.tdc('currency', $$.tr(ft, { class: cls || 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text(lbl)]), fnum(val, $rct.cst)), fn = (t) => $$.dc('fnote').insertAfter(tbl).rwText(t);
|
|
||||||
let csms = function (rrx, sms, sid) {
|
|
||||||
sms.tscn += (rrx.svcnet_val || 0);
|
|
||||||
sms.tscvat += (rrx.svcvat_val || 0);
|
|
||||||
sms.ttn += (rrx.net_val || 0);
|
|
||||||
sms.ttvat += (rrx.vat_val || 0);
|
|
||||||
sms.ttb += ((rrx.net_val || 0) + (rrx.vat_val || 0));
|
|
||||||
if ((rrx.vat || '') !== '') {
|
|
||||||
sms.vat[rrx.vat] = (sms.vat[rrx.vat] || 0) + (rrx.vat_val || 0);
|
|
||||||
}
|
|
||||||
//sms.itmnet[sid] = (sms.itmnet[sid] || 0) + (rrx.net_val || 0);
|
|
||||||
};
|
|
||||||
let bds = tbl.children('tbody');
|
let bds = tbl.children('tbody');
|
||||||
bds.each((bi, bdy) => {
|
bds.each((bi, bdy) => {
|
||||||
let b = $(bdy), rx = b.data() || {}, i = [], citems = [], cset = null, bnet = 0, itm = b.find('tr.itm'), iso = 0, ipos = 0;
|
let b = $(bdy), rx = b.data() || {}, i = [], citems = [], itm = b.find('tr.itm'), iso = 0, ipos = 0;
|
||||||
b.tC('empty', itm.length < 1);
|
b.tC('empty', itm.length < 1);
|
||||||
itm.each((ti, tx) => {
|
itm.each((ti, tx) => {
|
||||||
|
let rrx = $(tx).data() || {};
|
||||||
let rrx = $(tx).data() || {}; csms(rrx, sms, rx.Id); bnet += (rrx.net_val || 0); i.push(rrx.co);
|
i.push(rrx.co);
|
||||||
//console.debug('rrx %o', rrx);
|
|
||||||
/* backend item contract (title/desc/qty/price_net/total_net + set flags), see InvoiceSetPricing.
|
/* backend item contract (title/desc/qty/price_net/total_net + set flags), see InvoiceSetPricing.
|
||||||
Set grouping: an item of Type 'set' is a header that claims the following items in this
|
Set grouping: fds__prepInvoice computes [SetItmId] per item, anchored on the still-unconverted
|
||||||
block as its members until the next set header (mfr__items has no explicit member link). */
|
(price 0) Set header that owns it — this is the authoritative membership signal (not row order),
|
||||||
|
so items not tagged with a SetItmId by the server are never swept into a preceding set. The
|
||||||
|
header row's own SetItmId self-references its own id, but it is never its own member (excluded
|
||||||
|
by the id !== '' && !== citem.id check below). */
|
||||||
let citem = $inv.itemToContract(rrx);
|
let citem = $inv.itemToContract(rrx);
|
||||||
if (citem.type === 'set' && citem.id !== '') { cset = citem.id; }
|
let sid = (rrx.SetItmId || '').toString();
|
||||||
else if (cset !== null && (citem.id || '') !== '') { citem.setId = cset; }
|
if (citem.type !== 'set' && sid !== '' && sid !== citem.id) { citem.setId = sid; }
|
||||||
citems.push(citem);
|
citems.push(citem);
|
||||||
if (((typeof rrx.SortOrder === 'undefined' || rrx.SortOrder === null) ? -1 : rrx.SortOrder) > -1) {
|
if (((typeof rrx.SortOrder === 'undefined' || rrx.SortOrder === null) ? -1 : rrx.SortOrder) > -1) {
|
||||||
if (['text', 'title'].includes((rrx.Type || 'other').toLowerCase()) === false) { ipos++; }
|
if (['text', 'title'].includes((rrx.Type || 'other').toLowerCase()) === false) { ipos++; }
|
||||||
@@ -1671,41 +1737,10 @@ $inv.invSumUpdate = function () {
|
|||||||
$inv.rrw.call(tx);
|
$inv.rrw.call(tx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
//console.debug('%o', {
|
ba.push({ Id: rx.Id, nme: rx.Name, text: rx.text, itm: i, items: citems });
|
||||||
// f: b.find('tr.isum > td.isumval'), t: fnum(bnet, $rct.cst), n: bnet
|
|
||||||
//});
|
|
||||||
b.find('tr.isum > td.isumval').text(fnum(bnet, $rct.cst));
|
|
||||||
ba.push({ Id: rx.Id, nme: rx.Name, text: rx.text, itm: i, items: citems, netval: bnet });
|
|
||||||
});
|
});
|
||||||
let nonempty = tbl.find('tbody:not(.empty)').length;
|
let nonempty = tbl.find('tbody:not(.empty)').length;
|
||||||
bds.find('tr.isum').tC('hidden', nonempty < 2);
|
bds.find('tr.isum').tC('hidden', nonempty < 2);
|
||||||
//let fnet = $$.tdc('currency', $$.tr(ft, { class: 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text('Netto')]), fnum(sms.ttn, $rct.cst));
|
|
||||||
rwcy('Netto', sms.ttn);
|
|
||||||
if (p13b === false) {
|
|
||||||
$.each(sms.vat, (vi, vx) => {
|
|
||||||
//$$.tdc('currency vat', $$.tr(ft, { class: 'tvat' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text('Umsatzsteuer ' + vi)]), fnum(vx, $rct.cst));
|
|
||||||
rwcy($rct.vat + ' ' + vi, vx, 'tvat');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
sms.ttb = sms.ttn;
|
|
||||||
}
|
|
||||||
//let fsum = $$.tdc('currency', $$.tr(ft, { class: 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text('Summe')]), fnum(sms.ttb, $rct.cst));
|
|
||||||
rwcy('Summe', sms.ttb);
|
|
||||||
let itype = tbl.data().admin.type;
|
|
||||||
if (itype === 'i') {
|
|
||||||
fn($rct.note2);
|
|
||||||
fn($rct.note4);
|
|
||||||
} else if (itype === 'c') {
|
|
||||||
fn($rct.note2);
|
|
||||||
} else {
|
|
||||||
fn(string($rct.note3, [fnum((sms.tscn + sms.tscvat) * (tbl.data().admin.tax_servicerefund || 0), $rct.cst)])).aC('ntax');
|
|
||||||
fn($rct.note2);
|
|
||||||
fn(string($rct.note1, [fnum(sms.tscn + sms.tscvat, $rct.cst), fnum(sms.tscn, $rct.cst), fnum(sms.tscvat, $rct.cst)]));
|
|
||||||
}
|
|
||||||
if (p13b === true) {
|
|
||||||
fn($rct.note13b);
|
|
||||||
}
|
|
||||||
tbl.data('sms', sms);
|
|
||||||
tbl.data('bai', ba);
|
tbl.data('bai', ba);
|
||||||
/* Backend-authoritative seeding (ADR 0006): on the first calculation of an invoice
|
/* Backend-authoritative seeding (ADR 0006): on the first calculation of an invoice
|
||||||
draft (invSumUpdate is only bound for invoices, never reminders), hand the assembled
|
draft (invSumUpdate is only bound for invoices, never reminders), hand the assembled
|
||||||
@@ -1805,8 +1840,16 @@ $inv.t_fds_inv = () => {
|
|||||||
let tbl = $('div.invoice_layout table.invi');
|
let tbl = $('div.invoice_layout table.invi');
|
||||||
tbl.trigger('fds.inv');
|
tbl.trigger('fds.inv');
|
||||||
/* After any local item mutation, push the changed block(s) to the authoritative
|
/* After any local item mutation, push the changed block(s) to the authoritative
|
||||||
server session as granular deltas (invoice drafts only — reminders have no token). */
|
server session as granular deltas (invoice drafts only — reminders have no token,
|
||||||
if ((tbl.data('dtoken') || '') !== '') { $inv.d.syncChanged(tbl); }
|
nor a 'dseedpromise', so they correctly fall through to the no-op). While the initial
|
||||||
|
seed (inv/dopen) is still in flight there is no token yet either, but a 'dseedpromise'
|
||||||
|
is pending — wait for it instead of skipping the flush, otherwise an edit made in that
|
||||||
|
brief window is silently never sent. Returns the flush promise so callers that must
|
||||||
|
sequence a follow-up server delta after this flush (e.g. $inv.toSetPrice) can wait for
|
||||||
|
it instead of racing it. */
|
||||||
|
if ((tbl.data('dtoken') || '') !== '') { return $inv.d.syncChanged(tbl); }
|
||||||
|
let seeding = tbl.data('dseedpromise');
|
||||||
|
return seeding ? $.when(seeding).then(() => $inv.d.syncChanged(tbl)) : $.when();
|
||||||
};
|
};
|
||||||
$inv.sedit = () => {
|
$inv.sedit = () => {
|
||||||
$inv.sprev(true);
|
$inv.sprev(true);
|
||||||
@@ -1873,7 +1916,10 @@ $inv.itemToContract = function (rrx) {
|
|||||||
rrx = rrx || {};
|
rrx = rrx || {};
|
||||||
let oHtml = (e) => $$.d().append(e).html();
|
let oHtml = (e) => $$.d().append(e).html();
|
||||||
let type = (rrx.Type || '').toString().toLowerCase();
|
let type = (rrx.Type || '').toString().toLowerCase();
|
||||||
let ci = { id: (rrx.Id || '').toString(), type: type, title: '', desc: '', qty: '', price_net: '', total_net: (rrx.net_val || 0), vat: rrx.vat || '' };
|
/* ADR 0009: preserve null prices verbatim (null = empty cell, kept distinct from a real 0) so
|
||||||
|
block-mode nulled members and the set row round-trip unchanged when the block is re-posted. */
|
||||||
|
let nz = (x) => (x == null ? null : x);
|
||||||
|
let ci = { id: (rrx.Id || '').toString(), type: type, title: '', desc: '', qty: '', price_net: '', total_net: nz(rrx.net_val), vat: rrx.vat || '' };
|
||||||
if (rrx.co && rrx.co.typ === 'osum') {
|
if (rrx.co && rrx.co.typ === 'osum') {
|
||||||
/* combined single-sum line — the on-screen "title" is an HTML sub-table; render it as desc */
|
/* combined single-sum line — the on-screen "title" is an HTML sub-table; render it as desc */
|
||||||
ci.desc = rrx.co.t || '';
|
ci.desc = rrx.co.t || '';
|
||||||
@@ -1894,30 +1940,35 @@ $inv.itemToContract = function (rrx) {
|
|||||||
ci.desc = rrx.Note || '';
|
ci.desc = rrx.Note || '';
|
||||||
}
|
}
|
||||||
ci.qty = rrx.quantity || ((rrx.quantityhours || 0) !== 0 ? (fnum(rrx.quantityhours) + (rrx.UnitString ? ' ' + rrx.UnitString : '')) : '');
|
ci.qty = rrx.quantity || ((rrx.quantityhours || 0) !== 0 ? (fnum(rrx.quantityhours) + (rrx.UnitString ? ' ' + rrx.UnitString : '')) : '');
|
||||||
ci.price_net = (rrx.net || 0);
|
ci.price_net = nz(rrx.net);
|
||||||
ci.total_net = (rrx.net_val || 0);
|
ci.total_net = nz(rrx.net_val);
|
||||||
}
|
}
|
||||||
return ci;
|
return ci;
|
||||||
};
|
};
|
||||||
/* 3-way set-pricing display switch. Mirrors §13b: writes the choice onto admin.setmode,
|
/* 2-way set-pricing display switch (SetPrice / SetOnly). Mirrors §13b: writes the choice onto
|
||||||
which BuildInvoiceParams turns into the "setmode:<mode>" InvoiceOptions token the PDF reads. */
|
admin.setmode, which BuildInvoiceParams turns into the "setmode:<mode>" InvoiceOptions token
|
||||||
|
the PDF reads. ItemPrices ("Positionen mit Preis – Set als Überschrift") was removed: it was
|
||||||
|
always just the implicit pre-conversion default and is not a state the user can switch back to
|
||||||
|
once a set has been converted via the "Auf Setpreis umstellen" item switch (see $inv.toSetPrice /
|
||||||
|
InvoiceDraftEditService.ApplyItemSetPrice) — that conversion is one-way. */
|
||||||
$inv.ssetmode = () => {
|
$inv.ssetmode = () => {
|
||||||
let l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data();
|
let l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data();
|
||||||
d.admin = d.admin || {};
|
d.admin = d.admin || {};
|
||||||
let cur = (d.admin.setmode || 'setprice'), o;
|
let cur = (d.admin.setmode || 'setprice'), o;
|
||||||
let btn = (mode) => $$.dc('btn', $ict.setmo[mode]).tC('selected', cur === mode).click(() => { o.c.trigger('modal_close'); $inv.setSetmode(mode); });
|
let btn = (mode) => $$.dc('btn', $ict.setmo[mode]).tC('selected', cur === mode).click(() => { o.c.trigger('modal_close'); $inv.setSetmode(mode); });
|
||||||
let fr = $$.dc('choicefrm').append([btn('setprice'), btn('itemprices'), btn('setonly')]);
|
let fr = $$.dc('choicefrm').append([btn('setprice'), btn('setonly')]);
|
||||||
o = $ocms.dlg(fr, { width: 800 });
|
o = $ocms.dlg(fr, { width: 800 });
|
||||||
};
|
};
|
||||||
|
/* "Set mit Preis" / "Nur Set mit Preis" (ADR 0009): NOT a display toggle — an irreversible,
|
||||||
|
backend-authoritative mutation grouped by service-request block. The server inserts a dedicated
|
||||||
|
set row per block (block sum) and either nulls each member's price (setprice) or removes the
|
||||||
|
members (setonly); the normal draftReady refresh re-renders both via $inv.d.applyItems. The
|
||||||
|
browser only posts the chosen mode. Flush pending local edits first so a delayed block.replace
|
||||||
|
can't land after the conversion and overwrite it (same race guard as $inv.toSetPrice). */
|
||||||
$inv.setSetmode = (mode) => {
|
$inv.setSetmode = (mode) => {
|
||||||
let l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data();
|
if (confirm($rct.toSetMc || $rct.toSetPc) === false) { return; }
|
||||||
d.admin = d.admin || {};
|
let target = (mode === 'setonly') ? 'block.setonly' : 'block.setprice';
|
||||||
d.admin.setmode = mode; /* posted in admin -> BuildInvoiceParams writes setmode: into InvoiceOptions */
|
$.when($inv.t_fds_inv()).done(() => { $inv.d.sync({ Target: target }); });
|
||||||
d.inv = d.inv || {}; /* keep a local InvoiceOptions reflection in sync (cosmetic) */
|
|
||||||
let opts = (d.inv.InvoiceOptions || '').split(',').filter(x => x !== '' && x.indexOf('setmode:') !== 0);
|
|
||||||
if (mode && mode !== 'setprice') { opts.push('setmode:' + mode); }
|
|
||||||
d.inv.InvoiceOptions = opts.join(',');
|
|
||||||
$inv.d.sync({ Target: 'setmode', Value: mode });
|
|
||||||
};
|
};
|
||||||
$inv.sctp = () => {
|
$inv.sctp = () => {
|
||||||
let flds = $invcol.ctp;
|
let flds = $invcol.ctp;
|
||||||
@@ -1940,24 +1991,21 @@ $inv.sctp = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
/* Normalises the editor's working model into the exact field names the C# backend
|
/* Normalises the editor's working model into the exact field names the C# backend
|
||||||
(FdsInvoiceData.BuildInvoiceParams) reads, then returns the `invc` payload:
|
(FdsInvoiceData.BuildInvoiceParams) reads, then returns the `invc` payload. No totals/VAT
|
||||||
- balances/service sums come from `sms` (ttn/ttb), exposed on `new` as total_net/total_gross;
|
are computed here (ADR 0006/0008: the online editor performs no arithmetic at all) — the
|
||||||
- every VAT rate's net amount is exposed as new.vat_<rate>_net (the backend reads the highest);
|
backend session computes total_net/total_gross/vat itself (InvoiceDraftCalculator) once
|
||||||
|
opened from this payload, and BuildFdsData/BuildInvoiceParams reads them from there:
|
||||||
- new.invoicetitle -> new.title, new.loc -> new.provisionlocation, admin.paymentterms ->
|
- new.invoicetitle -> new.title, new.loc -> new.provisionlocation, admin.paymentterms ->
|
||||||
new.paymentterm, admin.CustomerId -> admin.customerid.
|
new.paymentterm, admin.CustomerId -> admin.customerid.
|
||||||
Originals are kept alongside; the source objects are not mutated. */
|
Originals are kept alongside; the source objects are not mutated. */
|
||||||
$inv.invcPayload = function (d) {
|
$inv.invcPayload = function (d) {
|
||||||
d = d || {};
|
d = d || {};
|
||||||
let sms = d.sms || {}, nw = $.extend({}, d.new), adm = $.extend({}, d.admin);
|
let nw = $.extend({}, d.new), adm = $.extend({}, d.admin);
|
||||||
nw.total_net = sms.ttn || 0;
|
|
||||||
nw.total_gross = sms.ttb || 0;
|
|
||||||
/* VAT (rate + amount) is taken by the backend straight from the posted sms.vat map
|
|
||||||
(FdsInvoiceData.HighestVat), so no per-rate new.vat_* keys are needed here. */
|
|
||||||
nw.title = (nw.invoicetitle != null ? nw.invoicetitle : (nw.title || ''));
|
nw.title = (nw.invoicetitle != null ? nw.invoicetitle : (nw.title || ''));
|
||||||
nw.provisionlocation = (nw.loc != null ? nw.loc : (nw.provisionlocation || ''));
|
nw.provisionlocation = (nw.loc != null ? nw.loc : (nw.provisionlocation || ''));
|
||||||
nw.paymentterm = (adm.paymentterms != null ? adm.paymentterms : (nw.paymentterm || ''));
|
nw.paymentterm = (adm.paymentterms != null ? adm.paymentterms : (nw.paymentterm || ''));
|
||||||
adm.customerid = (adm.customerid != null ? adm.customerid : adm.CustomerId);
|
adm.customerid = (adm.customerid != null ? adm.customerid : adm.CustomerId);
|
||||||
return { admin: adm, req: d.bai, sms: d.sms, new: nw };
|
return { admin: adm, req: d.bai, new: nw };
|
||||||
};
|
};
|
||||||
/* Zwischenspeichern and preview now run against the backend-authoritative session
|
/* Zwischenspeichern and preview now run against the backend-authoritative session
|
||||||
(ADR 0006): no full-invoice re-upload — the cached draft is flushed / rendered by
|
(ADR 0006): no full-invoice re-upload — the cached draft is flushed / rendered by
|
||||||
@@ -1971,20 +2019,11 @@ $inv.rReload = () => {
|
|||||||
$inv.cInv2({ id: s.search });
|
$inv.cInv2({ id: s.search });
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
};
|
};
|
||||||
$inv.quantChange = function (i) {
|
/* No client-side arithmetic (ADR 0006/0008): quantity/price/VAT edits are posted as raw values
|
||||||
//console.debug({ t: this, i: i });
|
and the server (InvoiceDraftCalculator.RecomputeLineValues) computes net_val/vat_val/
|
||||||
let t = $(this), f = t.closest('form'), fi = {}, pf = (i) => parseFloat(i.toString().replace('%', '').replace(',', '.')), rtp = (num) => num.toFixed(2);
|
svcnet_val/svcvat_val. The fields are left as entered here; the authoritative values are
|
||||||
f.find(':input').each((i, e) => { fi[$(e).attr('name')] = $(e); });
|
pushed back onto the row by $inv.d.applyItems once the change reaches the backend. */
|
||||||
let qv = parseInt(fi.quantityhours.val() || '0'), nv = pf(fi.net.val() || '0'), vat = pf(fi.vat.val()) * 0.01;
|
$inv.quantChange = function (i) { };
|
||||||
if (qv > 0 && nv > 0) {
|
|
||||||
fi.net_val.val(rtp(qv * nv));
|
|
||||||
fi.vat_val.val(rtp(qv * nv * vat));
|
|
||||||
if (['Service'].includes(fi.Type.val())) {
|
|
||||||
fi.svcnet_val.val(rtp(qv * nv));
|
|
||||||
fi.svcvat_val.val(rtp(qv * nv * vat));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
$inv.storno = function (id, fds) {
|
$inv.storno = function (id, fds) {
|
||||||
let o, fr = $$.dc('choicefrm').append([
|
let o, fr = $$.dc('choicefrm').append([
|
||||||
$$.dc('btn', 'Storno ohne Details').click({ id: id, mode: 'simple' }, (ev) => { o.c.trigger('modal_close'); $inv.cSt(ev.data); })
|
$$.dc('btn', 'Storno ohne Details').click({ id: id, mode: 'simple' }, (ev) => { o.c.trigger('modal_close'); $inv.cSt(ev.data); })
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -631,7 +631,7 @@ $ocms.postXT = function (options) {
|
|||||||
if ((typeof options.contentType === 'boolean' ? options.contentType === false : false) === true) {
|
if ((typeof options.contentType === 'boolean' ? options.contentType === false : false) === true) {
|
||||||
ajo.contentType = false; /* needed for file uploads */
|
ajo.contentType = false; /* needed for file uploads */
|
||||||
}
|
}
|
||||||
$.ajax(ajo);
|
return $.ajax(ajo);
|
||||||
};
|
};
|
||||||
$ocms.cex_timer = function () {
|
$ocms.cex_timer = function () {
|
||||||
if (!$ocms.cexi) {
|
if (!$ocms.cexi) {
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1,90 +0,0 @@
|
|||||||
using fds.Logging;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Topshelf;
|
|
||||||
|
|
||||||
namespace fds;
|
|
||||||
|
|
||||||
public class FdsService : ServiceControl
|
|
||||||
{
|
|
||||||
private readonly PeriodicHostedService _hostedService;
|
|
||||||
private readonly CancellationTokenSource _cts = new();
|
|
||||||
|
|
||||||
public FdsService()
|
|
||||||
{
|
|
||||||
var loggerFactory = LoggerFactory.Create(b =>
|
|
||||||
b.SetMinimumLevel(LogLevel.Debug).AddFdsLogging());
|
|
||||||
|
|
||||||
var mfr = new FdsMfr(
|
|
||||||
loggerFactory.CreateLogger<FdsMfr>(),
|
|
||||||
loggerFactory);
|
|
||||||
var interval = TimeSpan.FromMinutes(FdsConfig.ExecutionFrequency_Minutes);
|
|
||||||
var jobs = new[]
|
|
||||||
{
|
|
||||||
new PeriodicJobDefinition("MfrSync", interval, async ct =>
|
|
||||||
{
|
|
||||||
bool debug = FdsConfig.DebugDetails;
|
|
||||||
await mfr.UpdateIfNecessary_async(debug, ct);
|
|
||||||
await mfr.UpdateRequested_async(debug, ct);
|
|
||||||
await mfr.GetInvoiceFiles_async(debug, ct);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
var logger = loggerFactory.CreateLogger<PeriodicHostedService>();
|
|
||||||
_hostedService = new PeriodicHostedService(jobs, logger);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Start(HostControl hostControl)
|
|
||||||
{
|
|
||||||
_ = _hostedService.StartAsync(_cts.Token);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Stop(HostControl hostControl)
|
|
||||||
{
|
|
||||||
_cts.Cancel();
|
|
||||||
_hostedService.StopAsync(CancellationToken.None).GetAwaiter().GetResult();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class FdsMainModule
|
|
||||||
{
|
|
||||||
public static void Main(string[] args)
|
|
||||||
{
|
|
||||||
FdsConfig.Initialize();
|
|
||||||
|
|
||||||
string machineName = Environment.MachineName.ToLower();
|
|
||||||
if (!new[] { "digital-pc", "digital-dpc" }.Contains(machineName))
|
|
||||||
{
|
|
||||||
HostFactory.Run(x =>
|
|
||||||
{
|
|
||||||
x.Service<FdsService>(s =>
|
|
||||||
{
|
|
||||||
s.ConstructUsing(name => new FdsService());
|
|
||||||
s.WhenStarted((tc, host) => tc.Start(host));
|
|
||||||
s.WhenStopped((tc, host) => tc.Stop(host));
|
|
||||||
s.BeforeStoppingService(ctx =>
|
|
||||||
{
|
|
||||||
if (FdsConfig.DebugDetails) Task.Run(() => FdsDebug.DebugToFile("fds__data_service - beforestop", filename: "DebugDetail.txt"));
|
|
||||||
});
|
|
||||||
s.WhenPaused((tc, host) => tc.Stop(host));
|
|
||||||
s.WhenContinued((tc, host) => tc.Start(host));
|
|
||||||
});
|
|
||||||
x.EnablePauseAndContinue();
|
|
||||||
x.StartAutomatically();
|
|
||||||
x.RunAsLocalSystem();
|
|
||||||
x.SetDescription("MFR Data Sync");
|
|
||||||
x.SetDisplayName("MFR Data Sync");
|
|
||||||
x.SetServiceName("MFR Data Sync");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var svc = new FdsService();
|
|
||||||
svc.Start(null!);
|
|
||||||
Console.WriteLine("Running locally — press any key to stop.");
|
|
||||||
Console.ReadKey();
|
|
||||||
svc.Stop(null!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -319,17 +319,14 @@ public class FdsMfr : IFdsMfr
|
|||||||
var archiveFile = new FileInfo(Path.GetTempPath() + $"DatevUpload_AR {startdate:yyyyMM}_{admin["mode"]}.zip");
|
var archiveFile = new FileInfo(Path.GetTempPath() + $"DatevUpload_AR {startdate:yyyyMM}_{admin["mode"]}.zip");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var archive = new Archive(archiveFile, type: SevenZip.OutArchiveFormat.Zip,
|
// Native .NET zip via the OCORE helper (System.IO.Compression) — no external
|
||||||
logger: _loggerFactory.CreateLogger<Archive>());
|
// 7-Zip dependency. The DATEV export is a plain, unencrypted zip, so ZipArchive
|
||||||
if (archive.CompressToStream(fls, targetstream: stream!))
|
// covers it fully. filesToZipArchive skips null/empty entries.
|
||||||
{
|
var zipBytes = Task.Run(async () => await OCORE.zip.filesToZipArchive(fls)).Result;
|
||||||
stream!.Position = 0;
|
stream!.Write(zipBytes, 0, zipBytes.Length);
|
||||||
|
stream.Position = 0;
|
||||||
return archiveFile;
|
return archiveFile;
|
||||||
}
|
}
|
||||||
_logger.LogError(
|
|
||||||
"getDatevZip: CompressToStream returned false — archiveFile={ArchiveFile} fileCount={FileCount}",
|
|
||||||
archiveFile.FullName, fls.Count);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Previously swallowed entirely: a DATEV export could fail at the archive
|
// Previously swallowed entirely: a DATEV export could fail at the archive
|
||||||
|
|||||||
@@ -2,29 +2,22 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Microsoft.Data.SqlClient;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace fds;
|
namespace fds;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Holds the application <see cref="IConfiguration"/> built from appsettings.json.
|
/// Holds the application <see cref="IConfiguration"/> supplied by the host (the Fuchs web app).
|
||||||
/// Call <see cref="Initialize()"/> once at startup before accessing <see cref="Current"/>.
|
/// Fuchs_DataService is a library — it has no configuration of its own; the host owns the
|
||||||
|
/// connection strings and MFR credentials and injects them via <see cref="Initialize"/>,
|
||||||
|
/// which <c>Program.cs</c> calls once at startup before any <see cref="FdsMfr"/> use.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class FdsConfig
|
public static class FdsConfig
|
||||||
{
|
{
|
||||||
private static IConfiguration? _config;
|
private static IConfiguration? _config;
|
||||||
|
|
||||||
internal static IConfiguration Current =>
|
internal static IConfiguration Current =>
|
||||||
_config ?? throw new InvalidOperationException("FdsConfig has not been initialized. Call FdsConfig.Initialize() in Main().");
|
_config ?? throw new InvalidOperationException("FdsConfig has not been initialized. Call FdsConfig.Initialize(configuration) at host startup.");
|
||||||
|
|
||||||
public static void Initialize()
|
|
||||||
{
|
|
||||||
_config = new ConfigurationBuilder()
|
|
||||||
.SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
|
|
||||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false)
|
|
||||||
.Build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Initialize(IConfiguration configuration)
|
public static void Initialize(IConfiguration configuration)
|
||||||
{
|
{
|
||||||
@@ -32,21 +25,11 @@ public static class FdsConfig
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -- Connection strings ---------------------------------------------------
|
// -- Connection strings ---------------------------------------------------
|
||||||
internal static string SQLConnectionString() =>
|
|
||||||
Current.GetConnectionString("fuchs_ConnectionString")
|
|
||||||
?? throw new InvalidOperationException("Missing connection string: fuchs_ConnectionString");
|
|
||||||
|
|
||||||
internal static string FDSConnectionString() =>
|
internal static string FDSConnectionString() =>
|
||||||
Current.GetConnectionString("fuchs_fds_ConnectionString")
|
Current.GetConnectionString("fuchs_fds_ConnectionString")
|
||||||
?? throw new InvalidOperationException("Missing connection string: fuchs_fds_ConnectionString");
|
?? throw new InvalidOperationException("Missing connection string: fuchs_fds_ConnectionString");
|
||||||
|
|
||||||
// -- App settings ---------------------------------------------------------
|
// -- MFR credentials (supplied by the host from Key Vault / appsettings) --
|
||||||
internal static double ExecutionFrequency_Minutes =>
|
|
||||||
Current.GetValue<double>("Fds:ExecutionFrequency_Minutes", 15);
|
|
||||||
|
|
||||||
internal static bool DebugDetails =>
|
|
||||||
Current.GetValue<bool>("Fds:DebugDetails");
|
|
||||||
|
|
||||||
internal static string MFR_UserName =>
|
internal static string MFR_UserName =>
|
||||||
Current["Fds:MFR_UserName"] ?? "";
|
Current["Fds:MFR_UserName"] ?? "";
|
||||||
|
|
||||||
@@ -59,12 +42,8 @@ public static class FdsConfig
|
|||||||
|
|
||||||
internal static class FdsShared
|
internal static class FdsShared
|
||||||
{
|
{
|
||||||
internal static string SQLConnectionString() => FdsConfig.SQLConnectionString();
|
|
||||||
internal static string FDSConnectionString() => FdsConfig.FDSConnectionString();
|
internal static string FDSConnectionString() => FdsConfig.FDSConnectionString();
|
||||||
|
|
||||||
internal static SqlConnection SqlCon() =>
|
|
||||||
new(FdsConfig.SQLConnectionString());
|
|
||||||
|
|
||||||
public static string RandomString(byte length)
|
public static string RandomString(byte length)
|
||||||
{
|
{
|
||||||
var r = new Random();
|
var r = new Random();
|
||||||
|
|||||||
@@ -1,243 +0,0 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using SevenZip;
|
|
||||||
|
|
||||||
namespace fds;
|
|
||||||
|
|
||||||
public class Archive : IDisposable
|
|
||||||
{
|
|
||||||
public event Action? FileSaved;
|
|
||||||
public event Action? FileStreamCreated;
|
|
||||||
|
|
||||||
private FileInfo _archiveFile;
|
|
||||||
private string _archivePassword;
|
|
||||||
private OutArchiveFormat _archiveFormat;
|
|
||||||
private readonly ILogger<Archive> _logger;
|
|
||||||
public string TempPath { get; set; } = AppDomain.CurrentDomain.BaseDirectory;
|
|
||||||
public Stream? ArchiveFileStream { get; set; }
|
|
||||||
|
|
||||||
private SevenZipExtractor? _zipOut;
|
|
||||||
private SevenZipCompressor? _zipIn;
|
|
||||||
public bool ZipAppend { get; set; } = true;
|
|
||||||
public bool ExitOK { get; set; }
|
|
||||||
public bool ZipInOK { get; set; }
|
|
||||||
|
|
||||||
public Archive(FileInfo archiveFile, string archivePassword = "", bool init = true,
|
|
||||||
OutArchiveFormat type = OutArchiveFormat.SevenZip, ILogger<Archive>? logger = null)
|
|
||||||
{
|
|
||||||
_logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<Archive>.Instance;
|
|
||||||
_archiveFormat = type;
|
|
||||||
_archiveFile = new FileInfo(archiveFile.FullName.Replace(archiveFile.Extension,
|
|
||||||
type == OutArchiveFormat.SevenZip ? ".7z" : archiveFile.Extension));
|
|
||||||
_archivePassword = archivePassword;
|
|
||||||
if (init) InitZipIn(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitZipIn(OutArchiveFormat type)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(Zipping.SevenZipPath))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var assemblyDir = new DirectoryInfo(
|
|
||||||
new Uri(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)!).LocalPath);
|
|
||||||
var zip = assemblyDir.GetFiles("7z.dll", SearchOption.AllDirectories).FirstOrDefault();
|
|
||||||
Zipping.SevenZipPath = zip?.FullName ?? "";
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(Zipping.SevenZipPath))
|
|
||||||
{
|
|
||||||
var assemblyDir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory);
|
|
||||||
var zip = assemblyDir.GetFiles("7z.dll", SearchOption.AllDirectories).FirstOrDefault();
|
|
||||||
Zipping.SevenZipPath = zip?.FullName ?? "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (string.IsNullOrEmpty(Zipping.SevenZipPath))
|
|
||||||
_logger.LogError("SevenZipPath not found — 7z.dll is missing from the output directory.");
|
|
||||||
}
|
|
||||||
SevenZipCompressor.SetLibraryPath(Zipping.SevenZipPath);
|
|
||||||
|
|
||||||
_zipIn = new SevenZipCompressor
|
|
||||||
{
|
|
||||||
ArchiveFormat = (type == OutArchiveFormat.SevenZip && _archiveFile.Extension.Contains("7z", StringComparison.OrdinalIgnoreCase))
|
|
||||||
? OutArchiveFormat.SevenZip : type,
|
|
||||||
CompressionLevel = SevenZip.CompressionLevel.Ultra,
|
|
||||||
CompressionMode = ZipAppend ? SevenZip.CompressionMode.Append : SevenZip.CompressionMode.Create,
|
|
||||||
DirectoryStructure = false
|
|
||||||
};
|
|
||||||
|
|
||||||
_zipIn.CompressionMethod = _zipIn.ArchiveFormat switch
|
|
||||||
{
|
|
||||||
OutArchiveFormat.SevenZip => CompressionMethod.Lzma2,
|
|
||||||
OutArchiveFormat.Zip or OutArchiveFormat.GZip => CompressionMethod.Deflate,
|
|
||||||
_ => CompressionMethod.Default
|
|
||||||
};
|
|
||||||
|
|
||||||
ZipInOK = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Extract(FileInfo dataArchiveFilePath, DirectoryInfo tgtDirectory, OutArchiveFormat type = default)
|
|
||||||
{
|
|
||||||
if (!dataArchiveFilePath.Exists) return;
|
|
||||||
|
|
||||||
if (type == default)
|
|
||||||
type = dataArchiveFilePath.Extension.Contains("7z", StringComparison.OrdinalIgnoreCase)
|
|
||||||
? OutArchiveFormat.SevenZip : OutArchiveFormat.Zip;
|
|
||||||
|
|
||||||
if (!ZipInOK) InitZipIn(type);
|
|
||||||
|
|
||||||
_zipOut = string.IsNullOrEmpty(_archivePassword)
|
|
||||||
? new SevenZipExtractor(dataArchiveFilePath.FullName)
|
|
||||||
: new SevenZipExtractor(dataArchiveFilePath.FullName, _archivePassword);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!_zipOut.ArchiveFileData[0].Encrypted && !string.IsNullOrEmpty(_archivePassword))
|
|
||||||
_archivePassword = "";
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Archive.Extract failed — path={Path}, target={Target}",
|
|
||||||
dataArchiveFilePath.FullName, tgtDirectory.FullName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_zipOut.ExtractArchive(tgtDirectory.FullName);
|
|
||||||
_zipOut.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Compress(List<FileInfo> files, FileInfo? archiveFile = null, string? archivePass = null, OutArchiveFormat type = OutArchiveFormat.SevenZip)
|
|
||||||
{
|
|
||||||
if (files.Count == 0) return true;
|
|
||||||
if (!ZipInOK) InitZipIn(type);
|
|
||||||
|
|
||||||
archiveFile ??= _archiveFile;
|
|
||||||
archivePass = string.IsNullOrEmpty(archivePass) ? _archivePassword : archivePass;
|
|
||||||
|
|
||||||
if (archiveFile.Exists && ZipAppend)
|
|
||||||
_zipIn!.CompressionMode = CompressionMode.Append;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (archiveFile.Exists) archiveFile.Delete();
|
|
||||||
_zipIn!.CompressionMode = CompressionMode.Create;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var filesVerified = files.Where(f => f.Exists).ToArray();
|
|
||||||
var filePaths = filesVerified.Select(f => f.FullName).ToArray();
|
|
||||||
if (string.IsNullOrEmpty(archivePass))
|
|
||||||
_zipIn.CompressFiles(archiveFile.FullName, filePaths);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_zipIn.EncryptHeaders = true;
|
|
||||||
_zipIn.ZipEncryptionMethod = ZipEncryptionMethod.Aes256;
|
|
||||||
_zipIn.CompressFilesEncrypted(archiveFile.FullName, archivePass, filePaths);
|
|
||||||
}
|
|
||||||
FileSaved?.Invoke();
|
|
||||||
ExitOK = true;
|
|
||||||
_zipIn = null;
|
|
||||||
ZipInOK = false;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Previously silent: callers (e.g. HandleDatevZip) saw only a bare failed result
|
|
||||||
// with no way to tell a compression error from any other reason ExitOK is false.
|
|
||||||
_logger.LogError(ex, "Archive.Compress failed — archiveFile={ArchiveFile} fileCount={FileCount}",
|
|
||||||
archiveFile.FullName, files.Count);
|
|
||||||
ExitOK = false;
|
|
||||||
}
|
|
||||||
archiveFile.Refresh();
|
|
||||||
return ExitOK && archiveFile.Exists;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CompressToStream(Dictionary<string, byte[]> files, Stream? targetstream = null)
|
|
||||||
{
|
|
||||||
if (files.Count == 0) return true;
|
|
||||||
if (!ZipInOK) InitZipIn(_archiveFormat);
|
|
||||||
|
|
||||||
if (ArchiveFileStream == null)
|
|
||||||
{
|
|
||||||
_zipIn!.CompressionMode = CompressionMode.Create;
|
|
||||||
ArchiveFileStream = new MemoryStream();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
_zipIn!.CompressionMode = CompressionMode.Append;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var filesStreams = files.ToDictionary(kv => kv.Key, kv => (Stream)new MemoryStream(kv.Value));
|
|
||||||
var target = targetstream ?? ArchiveFileStream;
|
|
||||||
if (string.IsNullOrEmpty(_archivePassword))
|
|
||||||
_zipIn.CompressStreamDictionary(filesStreams, target);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_zipIn.EncryptHeaders = true;
|
|
||||||
_zipIn.ZipEncryptionMethod = ZipEncryptionMethod.Aes256;
|
|
||||||
_zipIn.CompressStreamDictionary(filesStreams, target, _archivePassword);
|
|
||||||
}
|
|
||||||
ArchiveFileStream.Seek(0, SeekOrigin.Begin);
|
|
||||||
FileStreamCreated?.Invoke();
|
|
||||||
ExitOK = true;
|
|
||||||
_zipIn = null;
|
|
||||||
ZipInOK = false;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Previously silent: the DATEV export ZIP-to-stream path failed with no
|
|
||||||
// trace anywhere, only a bare false return.
|
|
||||||
_logger.LogError(ex, "Archive.CompressToStream failed — fileCount={FileCount}", files.Count);
|
|
||||||
ExitOK = false;
|
|
||||||
}
|
|
||||||
return ExitOK;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool WriteArchiveStreamToDisk(FileInfo? archiveFile = null)
|
|
||||||
{
|
|
||||||
try { if (_archiveFile.Exists) _archiveFile.Delete(); } catch { }
|
|
||||||
FdsShared.WriteStreamToDisk(ArchiveFileStream!, (archiveFile ?? _archiveFile).FullName);
|
|
||||||
_archiveFile.Refresh();
|
|
||||||
return _archiveFile.Exists;
|
|
||||||
}
|
|
||||||
|
|
||||||
#region IDisposable
|
|
||||||
private bool _disposed;
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (!_disposed)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ArchiveFileStream?.Dispose();
|
|
||||||
_zipOut?.Dispose();
|
|
||||||
_zipIn = null;
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Zipping
|
|
||||||
{
|
|
||||||
public static string SevenZipPath = "";
|
|
||||||
|
|
||||||
public static void FastAppend(FileInfo fileToZip, FileInfo archiveFile)
|
|
||||||
{
|
|
||||||
if (fileToZip.Exists && archiveFile?.Exists == true)
|
|
||||||
{
|
|
||||||
using var zip = new Archive(archiveFile) { ZipAppend = true };
|
|
||||||
zip.Compress(new List<FileInfo> { fileToZip });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<RootNamespace>fds</RootNamespace>
|
<RootNamespace>fds</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
@@ -19,29 +18,18 @@
|
|||||||
<NoWarn>1591;NU1608</NoWarn>
|
<NoWarn>1591;NU1608</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="appsettings.json">
|
<!-- Expose internal members (FdsConfig, FdsShared helpers) to the test project -->
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<InternalsVisibleTo Include="Fuchs.Tests" />
|
||||||
</Content>
|
|
||||||
<Content Include="install.bat" />
|
|
||||||
<Content Include="un-install.bat" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\MFR_RESTClient\MFR_RESTClient.csproj" />
|
<ProjectReference Include="..\MFR_RESTClient\MFR_RESTClient.csproj" />
|
||||||
<ProjectReference Include="..\OCORE\OCORE\OCORE.csproj" />
|
<ProjectReference Include="..\OCORE\OCORE\OCORE.csproj" />
|
||||||
<ProjectReference Include="..\OCORE_web\OCORE_web\OCORE_web.csproj" />
|
<ProjectReference Include="..\OCORE_web\OCORE_web\OCORE_web.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
|
||||||
<Content Include="7z.dll">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||||
<PackageReference Include="Squid-Box.SevenZipSharp" Version="1.6.2.24" />
|
|
||||||
<PackageReference Include="Topshelf" Version="4.3.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
|
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.2" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.9" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.10" />
|
||||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.9" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.10" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<StartupObject>Sub Main</StartupObject>
|
|
||||||
<RootNamespace>fds</RootNamespace>
|
|
||||||
<MyType>Empty</MyType>
|
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
|
||||||
<Configurations>db-dev.processweb.de;Debug;Release;server02.processweb.de</Configurations>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Import Include="System" />
|
|
||||||
<Import Include="System.Data" />
|
|
||||||
<Import Include="Microsoft.Data" />
|
|
||||||
<Import Include="System.IO" />
|
|
||||||
</ItemGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
|
||||||
<PlatformTarget>x64</PlatformTarget>
|
|
||||||
<DocumentationFile>Fuchs_DataService.xml</DocumentationFile>
|
|
||||||
<NoWarn>42016,41999,42017,42018,42019,42032,42036,42020,42021,42022</NoWarn>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
|
||||||
<DefineDebug>false</DefineDebug>
|
|
||||||
<DocumentationFile>Fuchs_DataService.xml</DocumentationFile>
|
|
||||||
<NoWarn>42016,41999,42017,42018,42019,42032,42036,42020,42021,42022</NoWarn>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Update="My Project\Application.Designer.vb">
|
|
||||||
<AutoGen>True</AutoGen>
|
|
||||||
<DependentUpon>Application.myapp</DependentUpon>
|
|
||||||
<DesignTime>True</DesignTime>
|
|
||||||
</Compile>
|
|
||||||
<Compile Update="My Project\Resources.Designer.vb">
|
|
||||||
<AutoGen>True</AutoGen>
|
|
||||||
<DesignTime>True</DesignTime>
|
|
||||||
<DependentUpon>Resources.resx</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
<Compile Update="My Project\Settings.Designer.vb">
|
|
||||||
<AutoGen>True</AutoGen>
|
|
||||||
<DependentUpon>Settings.settings</DependentUpon>
|
|
||||||
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
|
||||||
</Compile>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Content Include="install.bat" />
|
|
||||||
<None Update="My Project\Application.myapp">
|
|
||||||
<Generator>MyApplicationCodeGenerator</Generator>
|
|
||||||
<LastGenOutput>Application.Designer.vb</LastGenOutput>
|
|
||||||
</None>
|
|
||||||
<Content Include="un-install.bat" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\MFR_RESTClient\MFR_RESTClient.csproj" />
|
|
||||||
<ProjectReference Include="..\..\..\WebProjectComponents\OCORE_web\OCORE_web.csproj" />
|
|
||||||
<ProjectReference Include="..\..\..\WebProjectComponents\OCORE\OCORE\OCORE.csproj" />
|
|
||||||
<ProjectReference Include="..\..\..\WebProjectComponents\OCMSsharp\OCMS\OCMS.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Content Include="7z.dll">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="Fuchs_DataService.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<!-- Updated packages -->
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
|
||||||
<!-- Compatible packages (kept) -->
|
|
||||||
<PackageReference Include="Squid-Box.SevenZipSharp" Version="1.6.1.23" />
|
|
||||||
<PackageReference Include="Topshelf" Version="4.3.0" />
|
|
||||||
<!-- New packages (needed for .NET 10) -->
|
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.0" />
|
|
||||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.5" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -18,38 +18,10 @@
|
|||||||
</member>
|
</member>
|
||||||
<member name="T:fds.FdsConfig">
|
<member name="T:fds.FdsConfig">
|
||||||
<summary>
|
<summary>
|
||||||
Holds the application <see cref="T:Microsoft.Extensions.Configuration.IConfiguration"/> built from appsettings.json.
|
Holds the application <see cref="T:Microsoft.Extensions.Configuration.IConfiguration"/> supplied by the host (the Fuchs web app).
|
||||||
Call <see cref="M:fds.FdsConfig.Initialize"/> once at startup before accessing <see cref="P:fds.FdsConfig.Current"/>.
|
Fuchs_DataService is a library — it has no configuration of its own; the host owns the
|
||||||
</summary>
|
connection strings and MFR credentials and injects them via <see cref="M:fds.FdsConfig.Initialize(Microsoft.Extensions.Configuration.IConfiguration)"/>,
|
||||||
</member>
|
which <c>Program.cs</c> calls once at startup before any <see cref="T:fds.FdsMfr"/> use.
|
||||||
<member name="T:fds.Logging.FdsLoggerProvider">
|
|
||||||
<summary>
|
|
||||||
Writes log entries to Debug output and a rolling file.
|
|
||||||
Database logging is wired up but disabled — set <see cref="P:fds.Logging.FdsLoggerProvider.DatabaseLoggingEnabled"/> to true to activate.
|
|
||||||
</summary>
|
|
||||||
</member>
|
|
||||||
<member name="P:fds.Logging.FdsLoggerProvider.DatabaseLoggingEnabled">
|
|
||||||
<summary>Set to true to activate database logging via fds__admin_logdebug.</summary>
|
|
||||||
</member>
|
|
||||||
<member name="M:fds.Logging.FdsLogger.WriteToDatabase(System.String,System.String,System.Exception)">
|
|
||||||
<summary>
|
|
||||||
Prepared DB logging via fds__admin_logdebug.
|
|
||||||
Enable by setting <see cref="P:fds.Logging.FdsLoggerProvider.DatabaseLoggingEnabled"/> = true.
|
|
||||||
</summary>
|
|
||||||
</member>
|
|
||||||
<member name="T:fds.PeriodicJobDefinition">
|
|
||||||
<summary>
|
|
||||||
Defines a named job with its own execution schedule for use with <see cref="T:fds.PeriodicHostedService"/>.
|
|
||||||
</summary>
|
|
||||||
</member>
|
|
||||||
<member name="M:fds.PeriodicJobDefinition.#ctor(System.String,System.TimeSpan,System.Func{System.Threading.CancellationToken,System.Threading.Tasks.Task})">
|
|
||||||
<summary>
|
|
||||||
Defines a named job with its own execution schedule for use with <see cref="T:fds.PeriodicHostedService"/>.
|
|
||||||
</summary>
|
|
||||||
</member>
|
|
||||||
<member name="T:fds.PeriodicHostedService">
|
|
||||||
<summary>
|
|
||||||
A <see cref="T:Microsoft.Extensions.Hosting.BackgroundService"/> that runs multiple independent jobs, each on its own <see cref="T:System.Threading.PeriodicTimer"/>.
|
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
</members>
|
</members>
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
namespace fds.Logging;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes log entries to Debug output and a rolling file.
|
|
||||||
/// Database logging is wired up but disabled — set <see cref="DatabaseLoggingEnabled"/> to true to activate.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class FdsLoggerProvider : ILoggerProvider
|
|
||||||
{
|
|
||||||
private readonly string _logDirectory;
|
|
||||||
|
|
||||||
/// <summary>Set to true to activate database logging via fds__admin_logdebug.</summary>
|
|
||||||
public static bool DatabaseLoggingEnabled { get; set; } = false;
|
|
||||||
|
|
||||||
public FdsLoggerProvider(string? logDirectory = null)
|
|
||||||
{
|
|
||||||
_logDirectory = logDirectory
|
|
||||||
?? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tmp");
|
|
||||||
Directory.CreateDirectory(_logDirectory);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ILogger CreateLogger(string categoryName) =>
|
|
||||||
new FdsLogger(categoryName, _logDirectory);
|
|
||||||
|
|
||||||
public void Dispose() { }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class FdsLogger : ILogger
|
|
||||||
{
|
|
||||||
private readonly string _categoryName;
|
|
||||||
private readonly string _logDirectory;
|
|
||||||
private static readonly Lock _fileLock = new();
|
|
||||||
|
|
||||||
internal FdsLogger(string categoryName, string logDirectory)
|
|
||||||
{
|
|
||||||
_categoryName = categoryName;
|
|
||||||
_logDirectory = logDirectory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
|
||||||
|
|
||||||
public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;
|
|
||||||
|
|
||||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
|
|
||||||
Exception? exception, Func<TState, Exception?, string> formatter)
|
|
||||||
{
|
|
||||||
if (!IsEnabled(logLevel)) return;
|
|
||||||
|
|
||||||
string message = formatter(state, exception);
|
|
||||||
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
|
||||||
string levelTag = logLevel switch
|
|
||||||
{
|
|
||||||
LogLevel.Trace => "TRC",
|
|
||||||
LogLevel.Debug => "DBG",
|
|
||||||
LogLevel.Information => "INF",
|
|
||||||
LogLevel.Warning => "WRN",
|
|
||||||
LogLevel.Error => "ERR",
|
|
||||||
LogLevel.Critical => "CRT",
|
|
||||||
_ => "???"
|
|
||||||
};
|
|
||||||
|
|
||||||
string line = $"{timestamp} [{levelTag}] {_categoryName}: {message}";
|
|
||||||
if (exception != null)
|
|
||||||
line += $"\r\n Exception: {exception.Message}\r\n Stack: {exception.StackTrace}";
|
|
||||||
|
|
||||||
// Always emit to Debug output
|
|
||||||
Debug.WriteLine(line);
|
|
||||||
|
|
||||||
// Always write to file
|
|
||||||
string filename = logLevel >= LogLevel.Error ? "ErrorLog.txt" : "DebugLog.txt";
|
|
||||||
AppendToFile(filename, line);
|
|
||||||
|
|
||||||
// Database logging — prepared, not activated
|
|
||||||
if (FdsLoggerProvider.DatabaseLoggingEnabled)
|
|
||||||
WriteToDatabase(_categoryName, message, exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AppendToFile(string filename, string line)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
lock (_fileLock)
|
|
||||||
File.AppendAllText(Path.Combine(_logDirectory, filename), line + "\r\n");
|
|
||||||
}
|
|
||||||
catch { /* never throw from logger */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prepared DB logging via fds__admin_logdebug.
|
|
||||||
/// Enable by setting <see cref="FdsLoggerProvider.DatabaseLoggingEnabled"/> = true.
|
|
||||||
/// </summary>
|
|
||||||
private static void WriteToDatabase(string codeReference, string message, Exception? exception)
|
|
||||||
{
|
|
||||||
// Activate by setting FdsLoggerProvider.DatabaseLoggingEnabled = true in appsettings / startup.
|
|
||||||
//
|
|
||||||
// using var con = new Microsoft.Data.SqlClient.SqlConnection(FdsConfig.FDSConnectionString());
|
|
||||||
// using var cmd = new Microsoft.Data.SqlClient.SqlCommand(
|
|
||||||
// "EXECUTE [dbo].[fds__admin_logdebug] @CodeReference, @ExceptionMessage, @StackTrace, @Data;", con);
|
|
||||||
// cmd.Parameters.AddWithValue("@CodeReference", codeReference);
|
|
||||||
// cmd.Parameters.AddWithValue("@ExceptionMessage", (object?)exception?.Message ?? DBNull.Value);
|
|
||||||
// cmd.Parameters.AddWithValue("@StackTrace", (object?)exception?.StackTrace ?? DBNull.Value);
|
|
||||||
// cmd.Parameters.AddWithValue("@Data", message);
|
|
||||||
// con.Open();
|
|
||||||
// cmd.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class FdsLoggingExtensions
|
|
||||||
{
|
|
||||||
public static ILoggingBuilder AddFdsLogging(this ILoggingBuilder builder, string? logDirectory = null)
|
|
||||||
{
|
|
||||||
builder.AddProvider(new FdsLoggerProvider(logDirectory));
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"fuchs_ConnectionString": "Data Source=MSSQL4.NBG4.DOMAINXYZ.DE,10439;Initial Catalog=site_fuchs_dev;Persist Security Info=False;TrustServerCertificate=true;Encrypt=true;User ID=fuchs_web;password='Bt5pL/cJg9oxb5';Connect Timeout=60;Load Balance Timeout=240;Max Pool Size=500;",
|
|
||||||
"fuchs_fds_ConnectionString": "Data Source=MSSQL4.NBG4.DOMAINXYZ.DE,10439;Initial Catalog=site_fuchs_dev;Persist Security Info=False;TrustServerCertificate=true;Encrypt=true;User ID=fuchs_fds;password='!Po@cGZ5bUn37khO';Connect Timeout=60;Load Balance Timeout=240;Max Pool Size=500;"
|
|
||||||
},
|
|
||||||
"Fds": {
|
|
||||||
"ExecutionFrequency_Minutes": 15,
|
|
||||||
"DebugDetails": true,
|
|
||||||
"MFR_UserName": "system@sebastian-fuchs---bad-und-heizung-gmbh-und-co-kg.com",
|
|
||||||
"MFR_Password": "0oT4G3H2",
|
|
||||||
"MFR_host": "portal.mobilefieldreport.com"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Fuchs_Dataservice.exe install --autostart
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Fuchs_Dataservice.exe uninstall
|
|
||||||
@@ -357,31 +357,6 @@
|
|||||||
<Build Include="dbo\Stored Procedures\fds__admin_removeconflicttables.sql" />
|
<Build Include="dbo\Stored Procedures\fds__admin_removeconflicttables.sql" />
|
||||||
<Build Include="dbo\Stored Procedures\fds__admin_logdebug.sql" />
|
<Build Include="dbo\Stored Procedures\fds__admin_logdebug.sql" />
|
||||||
<Build Include="dbo\Stored Procedures\fds__admin_getReportCatalog.sql" />
|
<Build Include="dbo\Stored Procedures\fds__admin_getReportCatalog.sql" />
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__setInvoicePayed.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__setBankingtransaction_autoAssigns.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__r_getBalanceTrendByYear.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__r_getBalanceTrendByMonth.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__r_getBalanceByYearTopMaterial.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__r_getBalanceByYearTopCustomer.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__r_getBalanceByMonth.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__prepStorno_recreate.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__prepReminder.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__lookupReminders.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__getRequests_list2.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__getRequests_list.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__getRequest_details.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__getReportDocument.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__getReminder.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__getInvRequestItems.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__getInvoices_list2.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__getInvoices_list_vario.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__getInvoiceReminder.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__getDatevExports.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__getBankingtransfers_questionable.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__getBankingtransfers.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__createStorno_simple.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__createReminder.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\backup__fds__createCredit_simple.sql" />
|
|
||||||
<Build Include="dbo\Stored Procedures\_BackupAndClearInvoiceFile.sql" />
|
<Build Include="dbo\Stored Procedures\_BackupAndClearInvoiceFile.sql" />
|
||||||
<Build Include="dbo\User Defined Types\json_data_maxU.sql" />
|
<Build Include="dbo\User Defined Types\json_data_maxU.sql" />
|
||||||
<Build Include="dbo\User Defined Types\hash_256.sql" />
|
<Build Include="dbo\User Defined Types\hash_256.sql" />
|
||||||
|
|||||||
@@ -1,264 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__createCredit_simple]
|
|
||||||
@InvId varchar(20)
|
|
||||||
, @authuser varchar(100)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
|
|
||||||
IF [dbo].[fis_getModuleAuth]('fds_inv', @authuser) < 2
|
|
||||||
THROW 60000, N'not authorized', 1;
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info])
|
|
||||||
VALUES ('fds__createCredit_simple' ,@authuser , 'invid: ' + ISNULL(@InvId, ''));
|
|
||||||
|
|
||||||
DECLARE @now datetime = GETDATE();
|
|
||||||
DECLARE @today date = CAST(@now as date);
|
|
||||||
|
|
||||||
DECLARE @newid varchar(10) = [dbo].[fds__fn_invoice_id]();
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @inv_DocumentName varchar(50), @inv_DateFinalized datetime, @inv_vat numeric(5,2), @inv_net numeric(10,3);
|
|
||||||
|
|
||||||
IF EXISTS (SELECT 0 FROM [dbo].[fds__invoices] WHERE [id] = @InvId AND ISNULL([isfinal],0) = 1)
|
|
||||||
BEGIN
|
|
||||||
|
|
||||||
SELECT @inv_DocumentName = [documentname], @inv_DateFinalized = [DateFinalized], @inv_vat = [InvoiceVAT_1], @inv_net = 1
|
|
||||||
FROM [dbo].[fds__invoices] WHERE [id] = @InvId
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__invoices]
|
|
||||||
([Id]
|
|
||||||
,[Version]
|
|
||||||
,[InvoiceId]
|
|
||||||
,[InvoiceType]
|
|
||||||
,[InvoiceTitle]
|
|
||||||
,[DocumentName]
|
|
||||||
,[InvoiceBalance]
|
|
||||||
,[InvoiceBalance_net]
|
|
||||||
,[InvoiceVAT_net1]
|
|
||||||
,[InvoiceVAT_1]
|
|
||||||
,[InvoiceVAT_net2]
|
|
||||||
,[InvoiceVAT_2]
|
|
||||||
,[PaymentTerm]
|
|
||||||
,[DueDate]
|
|
||||||
,[CustomerId]
|
|
||||||
,[SendToAddress]
|
|
||||||
,[SendToEmail]
|
|
||||||
,[ProvisionPeriod]
|
|
||||||
,[ProvisionLocation]
|
|
||||||
,[PaymentStatus]
|
|
||||||
,[IsPayed]
|
|
||||||
,[IsSent]
|
|
||||||
,[Replaces_InvId]
|
|
||||||
,[CustomValues]
|
|
||||||
,[DateSent]
|
|
||||||
,[UserSent]
|
|
||||||
,[DateFinalized]
|
|
||||||
,[UserFinalized]
|
|
||||||
,[DateCancelled]
|
|
||||||
,[UserCancelled]
|
|
||||||
,[DateCreated]
|
|
||||||
,[UserCreated]
|
|
||||||
,[DateModified]
|
|
||||||
,[UserModified])
|
|
||||||
SELECT
|
|
||||||
[Id] = @newid
|
|
||||||
,[Version] = 0
|
|
||||||
,[InvoiceId] = NULL
|
|
||||||
,[InvoiceType] = 'g'
|
|
||||||
,[InvoiceTitle] = 'Gutschrift'
|
|
||||||
,[DocumentName] = NULL
|
|
||||||
,[InvoiceBalance] = (@inv_net * -1) * (1 + 0.01 * ISNULL(@inv_vat,19))
|
|
||||||
,[InvoiceBalance_net] = (@inv_net * -1)
|
|
||||||
,[InvoiceVAT_net1] = (@inv_net * -1) * (0.01 * ISNULL(@inv_vat,19))
|
|
||||||
,[InvoiceVAT_1] = @inv_net
|
|
||||||
,[InvoiceVAT_net2] = null
|
|
||||||
,[InvoiceVAT_2] = null
|
|
||||||
,[PaymentTerm]
|
|
||||||
,[DueDate] = @today
|
|
||||||
,[CustomerId]
|
|
||||||
,[SendToAddress]
|
|
||||||
,[SendToEmail]
|
|
||||||
,[ProvisionPeriod]
|
|
||||||
,[ProvisionLocation]
|
|
||||||
,[PaymentStatus] = NULL
|
|
||||||
,[IsPayed] = 0
|
|
||||||
,[IsSent] = 0
|
|
||||||
,[Replaces_InvId] = @InvId
|
|
||||||
,[CustomValues]
|
|
||||||
,[DateSent] = NULL
|
|
||||||
,[UserSent] = @authuser
|
|
||||||
,[DateFinalized] = NULL
|
|
||||||
,[UserFinalized] = NULL
|
|
||||||
,[DateCancelled] = NULL
|
|
||||||
,[UserCancelled] = NULL
|
|
||||||
,[DateCreated] = @now
|
|
||||||
,[UserCreated] = @authuser
|
|
||||||
,[DateModified] = @now
|
|
||||||
,[UserModified] = @authuser
|
|
||||||
FROM [dbo].[fds__invoices] WHERE [id] = @InvId AND ISNULL([isfinal],0) = 1;
|
|
||||||
END
|
|
||||||
ELSE IF EXISTS (SELECT 0 FROM [dbo].[mfr__invoices] as _i where TRY_CAST(_i.[Id] as varchar(20)) = @InvId AND [FileType] in ('PdfInvoice','PdfPartialInvoice'))
|
|
||||||
BEGIN
|
|
||||||
|
|
||||||
SELECT @inv_DocumentName = [documentname], @inv_DateFinalized = [DateOfCreation]
|
|
||||||
, @inv_vat = CASE WHEN ISNULL([InvoiceBalanceNetto], 0) <> 0 THEN (([InvoiceBalance] / [InvoiceBalanceNetto]) - 1)*100 ELSE (SELECT TOP(1) TRY_CAST([value] as numeric(5,2)) FROM [dbo].[fds__admin_settings] as st where st.[type] = 'defaults' and st.[key] = 'vat') END
|
|
||||||
, @inv_net = 1 --[InvoiceBalanceNetto]
|
|
||||||
FROM [dbo].[mfr__invoices] as _i where TRY_CAST(_i.[Id] as varchar(20)) = @InvId AND [FileType] in ('PdfInvoice','PdfPartialInvoice');
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__invoices]
|
|
||||||
([Id]
|
|
||||||
,[Version]
|
|
||||||
,[InvoiceId]
|
|
||||||
,[InvoiceType]
|
|
||||||
,[InvoiceTitle]
|
|
||||||
,[DocumentName]
|
|
||||||
,[InvoiceBalance]
|
|
||||||
,[InvoiceBalance_net]
|
|
||||||
,[InvoiceVAT_net1]
|
|
||||||
,[InvoiceVAT_1]
|
|
||||||
,[InvoiceVAT_net2]
|
|
||||||
,[InvoiceVAT_2]
|
|
||||||
,[PaymentTerm]
|
|
||||||
,[DueDate]
|
|
||||||
,[CustomerId]
|
|
||||||
,[SendToAddress]
|
|
||||||
,[SendToEmail]
|
|
||||||
,[ProvisionPeriod]
|
|
||||||
,[ProvisionLocation]
|
|
||||||
,[PaymentStatus]
|
|
||||||
,[IsPayed]
|
|
||||||
,[IsSent]
|
|
||||||
,[Replaces_InvId]
|
|
||||||
,[CustomValues]
|
|
||||||
,[DateSent]
|
|
||||||
,[UserSent]
|
|
||||||
,[DateFinalized]
|
|
||||||
,[UserFinalized]
|
|
||||||
,[DateCancelled]
|
|
||||||
,[UserCancelled]
|
|
||||||
,[DateCreated]
|
|
||||||
,[UserCreated]
|
|
||||||
,[DateModified]
|
|
||||||
,[UserModified])
|
|
||||||
SELECT
|
|
||||||
[Id] = @newid
|
|
||||||
,[Version] = 0
|
|
||||||
,[InvoiceId] = NULL
|
|
||||||
,[InvoiceType] = 'g'
|
|
||||||
,[InvoiceTitle] = 'Gutschrift'
|
|
||||||
,[DocumentName] = NULL
|
|
||||||
,[InvoiceBalance] = (@inv_net * -1) * (1 + 0.01 * ISNULL(@inv_vat,19))
|
|
||||||
,[InvoiceBalance_net] = (@inv_net * -1)
|
|
||||||
,[InvoiceVAT_net1] = (@inv_net * -1) * (0.01 * ISNULL(@inv_vat,19))
|
|
||||||
,[InvoiceVAT_1] = @inv_vat
|
|
||||||
,[InvoiceVAT_net2] = NULL
|
|
||||||
,[InvoiceVAT_2] = NULL
|
|
||||||
,[PaymentTerm] = '10wd'
|
|
||||||
,[DueDate] = _i.DueDate
|
|
||||||
,[CustomerId] = (SELECT TOP(1) [partnerid] from [mfr__*PartnerSet] as ps where ps.[Property] = 'Invoice:Customer' and ps.[EntityId] = _i.[id])
|
|
||||||
,[SendToAddress] = NULL
|
|
||||||
,[SendToEmail] = NULL
|
|
||||||
,[ProvisionPeriod] = null
|
|
||||||
,[ProvisionLocation] = NULL
|
|
||||||
,[PaymentStatus] = NULL
|
|
||||||
,[IsPayed] = 0
|
|
||||||
,[IsSent] = 0
|
|
||||||
,[Replaces_InvId] = @InvId
|
|
||||||
,[CustomValues] = null
|
|
||||||
,[DateSent] = NULL
|
|
||||||
,[UserSent] = @authuser
|
|
||||||
,[DateFinalized] = NULL
|
|
||||||
,[UserFinalized] = NULL
|
|
||||||
,[DateCancelled] = NULL
|
|
||||||
,[UserCancelled] = NULL
|
|
||||||
,[DateCreated] = @now
|
|
||||||
,[UserCreated] = @authuser
|
|
||||||
,[DateModified] = @now
|
|
||||||
,[UserModified] = @authuser
|
|
||||||
FROM [dbo].[mfr__invoices] as _i where TRY_CAST(_i.[Id] as varchar(20)) = @InvId AND [FileType] in ('PdfInvoice','PdfPartialInvoice');
|
|
||||||
UPDATE [dbo].[fds__invoices]
|
|
||||||
SET [SendToAddress] = [dbo].[fds__getCompanyNameAddress]([CustomerId]), [SendToEmail] = [dbo].[fds__getCompanyEmail]([CustomerId])
|
|
||||||
WHERE [Id] = @newid;
|
|
||||||
|
|
||||||
END
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- continue creating storno
|
|
||||||
|
|
||||||
DECLARE @inv_srq [dbo].[fds__tt__invoice_servicerequests];
|
|
||||||
INSERT INTO [dbo].[fds__invoice_servicerequests]
|
|
||||||
([InvId]
|
|
||||||
,[mfr__servicerequest]
|
|
||||||
,[title]
|
|
||||||
,[value_net]
|
|
||||||
,[SortOrder])
|
|
||||||
OUTPUT
|
|
||||||
inserted.* INTO @inv_srq
|
|
||||||
SELECT
|
|
||||||
[InvId] = [id]
|
|
||||||
,NULL --<mfr__servicerequest, bigint,>
|
|
||||||
,'Gutschrift'
|
|
||||||
,(@inv_net * -1)
|
|
||||||
,0
|
|
||||||
FROM [dbo].[fds__invoices] WHERE [id] = @newid;
|
|
||||||
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__invoice_items]
|
|
||||||
([InvId]
|
|
||||||
,[InvRqId]
|
|
||||||
,[mfr__item]
|
|
||||||
,[reference_InvId]
|
|
||||||
,[Type]
|
|
||||||
,[Position]
|
|
||||||
,[Quantity]
|
|
||||||
,[Text]
|
|
||||||
,[value]
|
|
||||||
,[value_total]
|
|
||||||
,[vat]
|
|
||||||
,[value_service]
|
|
||||||
,[det]
|
|
||||||
,[SortOrder])
|
|
||||||
SELECT
|
|
||||||
@newid --<InvId, varchar(10),>
|
|
||||||
,[id]--<InvRqId, bigint,>
|
|
||||||
,NULL --<mfr__item, bigint,>
|
|
||||||
,@InvId --<reference_InvId, varchar(20),>
|
|
||||||
,'CreditInvoice' --<Type, nvarchar(255),>
|
|
||||||
,NULL --<Position, varchar(5),>
|
|
||||||
,'1' ---<Quantity, nvarchar(25),>
|
|
||||||
,[Text] = '<p>' + 'Gutschrift zu der ' + REPLACE(@inv_DocumentName, '.pdf','') + ' vom ' + FORMAT(@inv_dateFinalized, 'dd.MM.yyyy', 'de-de') + '</p>'
|
|
||||||
,[value] = (@inv_net * -1)
|
|
||||||
,[value_total] = (@inv_net * -1)
|
|
||||||
,[vat] = @inv_vat
|
|
||||||
,0 --<value_service, numeric(10,3),>
|
|
||||||
,1 --<det, bit,>
|
|
||||||
,1 --<SortOrder, tinyint,>)
|
|
||||||
FROM @inv_srq;
|
|
||||||
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__invoice_details]
|
|
||||||
([InvId]
|
|
||||||
,[InvoiceService_net]
|
|
||||||
,[InvoiceService_VAT]
|
|
||||||
,[AllocatedTo_InvId]
|
|
||||||
,[StornoTo_InvId])
|
|
||||||
VALUES(
|
|
||||||
@newid
|
|
||||||
,NULL --[InvoiceService_net]
|
|
||||||
,NULL --[InvoiceService_VAT]
|
|
||||||
,NULL --[AllocatedTo_InvId]
|
|
||||||
,@InvId --[StornoTo_InvId]
|
|
||||||
);
|
|
||||||
|
|
||||||
EXECUTE [dbo].[fds__getInvoice] @newid, @authuser;
|
|
||||||
|
|
||||||
|
|
||||||
END
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__createReminder]
|
|
||||||
@InvId varchar(15)
|
|
||||||
, @type varchar(3)
|
|
||||||
, @amount numeric(10,3)
|
|
||||||
, @amount_payed numeric(10,3)
|
|
||||||
, @SendToAddress [nvarchar](1000)
|
|
||||||
, @SendToEmail [nvarchar](255)
|
|
||||||
, @subject nvarchar(255)
|
|
||||||
, @text nvarchar(2000)
|
|
||||||
, @authuser varchar(25)
|
|
||||||
, @Id varchar(10) OUT
|
|
||||||
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
|
|
||||||
|
|
||||||
IF [dbo].[fis_getModuleAuth]('fds_reminder', @authuser) < 2
|
|
||||||
THROW 60000, N'not authorized', 1;
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info])
|
|
||||||
VALUES ('fds__createReminder' ,@authuser , 'invid: ' + ISNULL(@invid, ''));
|
|
||||||
|
|
||||||
DECLARE @now datetime = GETUTCDATE();
|
|
||||||
DECLARE @today date = @now;
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @newid varchar(10) = [dbo].[fds__fn_reminder_id]();
|
|
||||||
|
|
||||||
DECLARE @OUT [dbo].[fds__tt__reminder_core];
|
|
||||||
DECLARE @CustomerId bigint = dbo.fds__fn_invoice_customerid(@InvId);
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__reminder]
|
|
||||||
([Id]
|
|
||||||
,[version]
|
|
||||||
,[InvId]
|
|
||||||
,[CustomerId]
|
|
||||||
,[SendToAddress]
|
|
||||||
,[SendToEmail]
|
|
||||||
,[type]
|
|
||||||
,[amount]
|
|
||||||
,[amount_payed]
|
|
||||||
,[subject]
|
|
||||||
,[text]
|
|
||||||
,[IsSent]
|
|
||||||
,[CustomValues]
|
|
||||||
,[DateSent]
|
|
||||||
,[UserSent]
|
|
||||||
,[DateFinalized]
|
|
||||||
,[UserFinalized]
|
|
||||||
,[DateCreated]
|
|
||||||
,[UserCreated]
|
|
||||||
,[DateModified]
|
|
||||||
,[UserModified]
|
|
||||||
,[file])
|
|
||||||
OUTPUT inserted.*
|
|
||||||
INTO @OUT
|
|
||||||
VALUES
|
|
||||||
(@newid
|
|
||||||
,0 --version
|
|
||||||
,@InvId
|
|
||||||
,@CustomerId
|
|
||||||
,@SendToAddress
|
|
||||||
,@SendToEmail
|
|
||||||
,@type
|
|
||||||
,@amount
|
|
||||||
,@amount_payed
|
|
||||||
,@subject
|
|
||||||
,@text
|
|
||||||
,0 --IsSent, bit,>
|
|
||||||
,null --<CustomValues, nvarchar(max),>
|
|
||||||
,NULL --<DateSent, datetime,>
|
|
||||||
,NULL --[UserSent]
|
|
||||||
,NULL --<DateFinalized, datetime,>
|
|
||||||
,NULL --<UserFinalized, varchar(25),>
|
|
||||||
,@now
|
|
||||||
,@authuser
|
|
||||||
,@now
|
|
||||||
,@authuser
|
|
||||||
,NULL);
|
|
||||||
|
|
||||||
|
|
||||||
SELECT TOP(1) @Id = [id] FROM @out;
|
|
||||||
|
|
||||||
|
|
||||||
With inv1 as(
|
|
||||||
select TOP(1) * FROM [dbo].[fds__invoices] WHERE [id] = @InvId
|
|
||||||
), inv as(
|
|
||||||
SELECT [Id] = cast([id] as varchar(15))
|
|
||||||
,[Invoiceid]
|
|
||||||
,[DateFinalized]
|
|
||||||
,[InvoiceBalance]
|
|
||||||
FROM inv1
|
|
||||||
UNION
|
|
||||||
SELECT [Id] = cast([id] as varchar(15))
|
|
||||||
,[Invoiceid]
|
|
||||||
,[DateFinalized] = [DateOfCreation]
|
|
||||||
,[InvoiceBalance]
|
|
||||||
FROM [dbo].[mfr__invoices] as mfri
|
|
||||||
WHERE [id] = TRY_CAST(@invId as bigint) AND NOT EXISTS( SELECT 0 FROM inv1 ) --will only be used, if id not present in fds__invoices
|
|
||||||
)
|
|
||||||
SELECT TOP(1)
|
|
||||||
o.*
|
|
||||||
, [InvoiceId] = inv.[InvoiceId]
|
|
||||||
, [InvoiceDate] = inv.[DateFinalized]
|
|
||||||
, [hasFile] = CAST ( CASE WHEN o.[file] is null THEN 0 ELSE 1 END as bit)
|
|
||||||
, [UserNameFinalized] = [dbo].[fis_admin_getUserName_byID](o.[UserFinalized])
|
|
||||||
, [UserEmailFinalized] = [dbo].[fis_admin_getUserEmail_byID](o.[UserFinalized])
|
|
||||||
from @out as o
|
|
||||||
join inv on o.[invid] = inv.[id];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
END
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__createStorno_simple]
|
|
||||||
@InvId varchar(20)
|
|
||||||
, @authuser varchar(100)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
|
|
||||||
IF [dbo].[fis_getModuleAuth]('fds_inv', @authuser) < 2
|
|
||||||
THROW 60000, N'not authorized', 1;
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info])
|
|
||||||
VALUES ('fds__createStorno_simple' ,@authuser , 'invid: ' + ISNULL(@InvId, ''));
|
|
||||||
|
|
||||||
DECLARE @now datetime = GETDATE();
|
|
||||||
DECLARE @today date = CAST(@now as date);
|
|
||||||
|
|
||||||
DECLARE @newid varchar(10) = [dbo].[fds__fn_invoice_id]();
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @inv_DocumentName varchar(50), @inv_DateFinalized datetime, @inv_vat numeric(5,2), @inv_net numeric(10,3);
|
|
||||||
|
|
||||||
IF EXISTS (SELECT 0 FROM [dbo].[fds__invoices] WHERE [id] = @InvId AND ISNULL([isfinal],0) = 1)
|
|
||||||
BEGIN
|
|
||||||
|
|
||||||
SELECT @inv_DocumentName = [documentname], @inv_DateFinalized = [DateFinalized], @inv_vat = [InvoiceVAT_1], @inv_net = [invoicebalance_net] FROM [dbo].[fds__invoices] WHERE [id] = @InvId
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__invoices]
|
|
||||||
([Id]
|
|
||||||
,[Version]
|
|
||||||
,[InvoiceId]
|
|
||||||
,[InvoiceType]
|
|
||||||
,[InvoiceTitle]
|
|
||||||
,[DocumentName]
|
|
||||||
,[InvoiceBalance]
|
|
||||||
,[InvoiceBalance_net]
|
|
||||||
,[InvoiceVAT_net1]
|
|
||||||
,[InvoiceVAT_1]
|
|
||||||
,[InvoiceVAT_net2]
|
|
||||||
,[InvoiceVAT_2]
|
|
||||||
,[PaymentTerm]
|
|
||||||
,[DueDate]
|
|
||||||
,[CustomerId]
|
|
||||||
,[SendToAddress]
|
|
||||||
,[SendToEmail]
|
|
||||||
,[ProvisionPeriod]
|
|
||||||
,[ProvisionLocation]
|
|
||||||
,[PaymentStatus]
|
|
||||||
,[IsPayed]
|
|
||||||
,[IsSent]
|
|
||||||
,[Replaces_InvId]
|
|
||||||
,[CustomValues]
|
|
||||||
,[DateSent]
|
|
||||||
,[UserSent]
|
|
||||||
,[DateFinalized]
|
|
||||||
,[UserFinalized]
|
|
||||||
,[DateCancelled]
|
|
||||||
,[UserCancelled]
|
|
||||||
,[DateCreated]
|
|
||||||
,[UserCreated]
|
|
||||||
,[DateModified]
|
|
||||||
,[UserModified])
|
|
||||||
SELECT
|
|
||||||
[Id] = @newid
|
|
||||||
,[Version] = 0
|
|
||||||
,[InvoiceId] = NULL
|
|
||||||
,[InvoiceType] = 'c'
|
|
||||||
,[InvoiceTitle] = 'Stornorechnung'
|
|
||||||
,[DocumentName] = NULL
|
|
||||||
,[InvoiceBalance] = ([InvoiceBalance] * -1)
|
|
||||||
,[InvoiceBalance_net] = ([InvoiceBalance_net] *-1)
|
|
||||||
,[InvoiceVAT_net1] = ([InvoiceVAT_net1] * -1)
|
|
||||||
,[InvoiceVAT_1]
|
|
||||||
,[InvoiceVAT_net2] = ([InvoiceVAT_net2] * -1)
|
|
||||||
,[InvoiceVAT_2]
|
|
||||||
,[PaymentTerm]
|
|
||||||
,[DueDate] = @today
|
|
||||||
,[CustomerId]
|
|
||||||
,[SendToAddress]
|
|
||||||
,[SendToEmail]
|
|
||||||
,[ProvisionPeriod]
|
|
||||||
,[ProvisionLocation]
|
|
||||||
,[PaymentStatus] = NULL
|
|
||||||
,[IsPayed] = 0
|
|
||||||
,[IsSent] = 0
|
|
||||||
,[Replaces_InvId] = @InvId
|
|
||||||
,[CustomValues]
|
|
||||||
,[DateSent] = NULL
|
|
||||||
,[UserSent] = NULL
|
|
||||||
,[DateFinalized] = NULL
|
|
||||||
,[UserFinalized] = NULL
|
|
||||||
,[DateCancelled] = NULL
|
|
||||||
,[UserCancelled] = NULL
|
|
||||||
,[DateCreated] = @now
|
|
||||||
,[UserCreated] = @authuser
|
|
||||||
,[DateModified] = @now
|
|
||||||
,[UserModified] = @authuser
|
|
||||||
FROM [dbo].[fds__invoices] WHERE [id] = @InvId AND ISNULL([isfinal],0) = 1;
|
|
||||||
END
|
|
||||||
ELSE IF EXISTS (SELECT 0 FROM [dbo].[mfr__invoices] as _i where TRY_CAST(_i.[Id] as varchar(20)) = @InvId AND [FileType] in ('PdfInvoice','PdfPartialInvoice'))
|
|
||||||
BEGIN
|
|
||||||
|
|
||||||
SELECT @inv_DocumentName = [documentname], @inv_DateFinalized = [DateOfCreation]
|
|
||||||
, @inv_vat = CASE WHEN ISNULL([InvoiceBalanceNetto], 0) <> 0 THEN (([InvoiceBalance] / [InvoiceBalanceNetto]) - 1)*100 ELSE (SELECT TOP(1) TRY_CAST([value] as numeric(5,2)) FROM [dbo].[fds__admin_settings] as st where st.[type] = 'defaults' and st.[key] = 'vat') END
|
|
||||||
, @inv_net = [InvoiceBalanceNetto]
|
|
||||||
FROM [dbo].[mfr__invoices] as _i where TRY_CAST(_i.[Id] as varchar(20)) = @InvId AND [FileType] in ('PdfInvoice','PdfPartialInvoice');
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__invoices]
|
|
||||||
([Id]
|
|
||||||
,[Version]
|
|
||||||
,[InvoiceId]
|
|
||||||
,[InvoiceType]
|
|
||||||
,[InvoiceTitle]
|
|
||||||
,[DocumentName]
|
|
||||||
,[InvoiceBalance]
|
|
||||||
,[InvoiceBalance_net]
|
|
||||||
,[InvoiceVAT_net1]
|
|
||||||
,[InvoiceVAT_1]
|
|
||||||
,[InvoiceVAT_net2]
|
|
||||||
,[InvoiceVAT_2]
|
|
||||||
,[PaymentTerm]
|
|
||||||
,[DueDate]
|
|
||||||
,[CustomerId]
|
|
||||||
,[SendToAddress]
|
|
||||||
,[SendToEmail]
|
|
||||||
,[ProvisionPeriod]
|
|
||||||
,[ProvisionLocation]
|
|
||||||
,[PaymentStatus]
|
|
||||||
,[IsPayed]
|
|
||||||
,[IsSent]
|
|
||||||
,[Replaces_InvId]
|
|
||||||
,[CustomValues]
|
|
||||||
,[DateSent]
|
|
||||||
,[UserSent]
|
|
||||||
,[DateFinalized]
|
|
||||||
,[UserFinalized]
|
|
||||||
,[DateCancelled]
|
|
||||||
,[UserCancelled]
|
|
||||||
,[DateCreated]
|
|
||||||
,[UserCreated]
|
|
||||||
,[DateModified]
|
|
||||||
,[UserModified])
|
|
||||||
SELECT
|
|
||||||
[Id] = @newid
|
|
||||||
,[Version] = 0
|
|
||||||
,[InvoiceId] = NULL
|
|
||||||
,[InvoiceType] = 'c'
|
|
||||||
,[InvoiceTitle] = 'Stornorechnung'
|
|
||||||
,[DocumentName] = NULL
|
|
||||||
,[InvoiceBalance] = ([InvoiceBalance] * -1)
|
|
||||||
,[InvoiceBalance_net] = ([InvoiceBalanceNetto] *-1)
|
|
||||||
,[InvoiceVAT_net1] = ((ISNULL([InvoiceBalance],0) - ISNULL([InvoiceBalanceNetto],0)) * -1)
|
|
||||||
,[InvoiceVAT_1] = @inv_vat
|
|
||||||
,[InvoiceVAT_net2] = NULL
|
|
||||||
,[InvoiceVAT_2] = NULL
|
|
||||||
,[PaymentTerm] = '10wd'
|
|
||||||
,[DueDate] = _i.DueDate
|
|
||||||
,[CustomerId] = (SELECT TOP(1) [partnerid] from [mfr__*PartnerSet] as ps where ps.[Property] = 'Invoice:Customer' and ps.[EntityId] = _i.[id])
|
|
||||||
,[SendToAddress] = NULL
|
|
||||||
,[SendToEmail] = NULL
|
|
||||||
,[ProvisionPeriod] = null
|
|
||||||
,[ProvisionLocation] = NULL
|
|
||||||
,[PaymentStatus] = NULL
|
|
||||||
,[IsPayed] = 0
|
|
||||||
,[IsSent] = 0
|
|
||||||
,[Replaces_InvId] = @InvId
|
|
||||||
,[CustomValues] = null
|
|
||||||
,[DateSent] = NULL
|
|
||||||
,[UserSent] = NULL
|
|
||||||
,[DateFinalized] = NULL
|
|
||||||
,[UserFinalized] = NULL
|
|
||||||
,[DateCancelled] = NULL
|
|
||||||
,[UserCancelled] = NULL
|
|
||||||
,[DateCreated] = @now
|
|
||||||
,[UserCreated] = @authuser
|
|
||||||
,[DateModified] = @now
|
|
||||||
,[UserModified] = @authuser
|
|
||||||
FROM [dbo].[mfr__invoices] as _i where TRY_CAST(_i.[Id] as varchar(20)) = @InvId AND [FileType] in ('PdfInvoice','PdfPartialInvoice');
|
|
||||||
UPDATE [dbo].[fds__invoices]
|
|
||||||
SET [SendToAddress] = [dbo].[fds__getCompanyNameAddress]([CustomerId]), [SendToEmail] = [dbo].[fds__getCompanyEmail]([CustomerId])
|
|
||||||
WHERE [Id] = @newid;
|
|
||||||
|
|
||||||
END
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- continue creating storno
|
|
||||||
|
|
||||||
DECLARE @inv_srq [dbo].[fds__tt__invoice_servicerequests];
|
|
||||||
INSERT INTO [dbo].[fds__invoice_servicerequests]
|
|
||||||
([InvId]
|
|
||||||
,[mfr__servicerequest]
|
|
||||||
,[title]
|
|
||||||
,[value_net]
|
|
||||||
,[SortOrder])
|
|
||||||
OUTPUT
|
|
||||||
inserted.* INTO @inv_srq
|
|
||||||
SELECT TOP(1)
|
|
||||||
[InvId] = [id]
|
|
||||||
,NULL --<mfr__servicerequest, bigint,>
|
|
||||||
,'Storno'
|
|
||||||
,0
|
|
||||||
,0
|
|
||||||
FROM [dbo].[fds__invoices] WHERE [id] = @newid;
|
|
||||||
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__invoice_items]
|
|
||||||
([InvId]
|
|
||||||
,[InvRqId]
|
|
||||||
,[mfr__item]
|
|
||||||
,[reference_InvId]
|
|
||||||
,[Type]
|
|
||||||
,[Position]
|
|
||||||
,[Quantity]
|
|
||||||
,[Text]
|
|
||||||
,[value]
|
|
||||||
,[value_total]
|
|
||||||
,[vat]
|
|
||||||
,[value_service]
|
|
||||||
,[det]
|
|
||||||
,[SortOrder])
|
|
||||||
SELECT
|
|
||||||
@newid --<InvId, varchar(10),>
|
|
||||||
,[id]--<InvRqId, bigint,>
|
|
||||||
,NULL --<mfr__item, bigint,>
|
|
||||||
,@InvId --<reference_InvId, varchar(20),>
|
|
||||||
,'CanceledInvoice' --<Type, nvarchar(255),>
|
|
||||||
,NULL --<Position, varchar(5),>
|
|
||||||
,'1' ---<Quantity, nvarchar(25),>
|
|
||||||
,[Text] = '<p>' + 'Storno der ' + REPLACE(@inv_DocumentName, '.pdf','') + ' vom ' + FORMAT(@inv_dateFinalized, 'dd.MM.yyyy', 'de-de') + '</p>'
|
|
||||||
,[value] = (@inv_net * -1)
|
|
||||||
,[value_total] = (@inv_net * -1)
|
|
||||||
,[vat] = @inv_vat
|
|
||||||
,0 --<value_service, numeric(10,3),>
|
|
||||||
,1 --<det, bit,>
|
|
||||||
,1 --<SortOrder, tinyint,>)
|
|
||||||
FROM @inv_srq;
|
|
||||||
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__invoice_details]
|
|
||||||
([InvId]
|
|
||||||
,[InvoiceService_net]
|
|
||||||
,[InvoiceService_VAT]
|
|
||||||
,[AllocatedTo_InvId]
|
|
||||||
,[StornoTo_InvId])
|
|
||||||
VALUES(
|
|
||||||
@newid
|
|
||||||
,NULL --[InvoiceService_net]
|
|
||||||
,NULL --[InvoiceService_VAT]
|
|
||||||
,NULL --[AllocatedTo_InvId]
|
|
||||||
,@InvId --[StornoTo_InvId]
|
|
||||||
);
|
|
||||||
|
|
||||||
EXECUTE [dbo].[fds__getInvoice] @newid, @authuser;
|
|
||||||
|
|
||||||
|
|
||||||
END
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
|
|
||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__getBankingtransfers]
|
|
||||||
@mode varchar(1) = 'm'
|
|
||||||
, @authuser varchar(100)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
IF [dbo].[fis_getModuleAuth]('fds_bam', @authuser) < 1
|
|
||||||
THROW 60000, N'not authorized', 1;
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info])
|
|
||||||
VALUES ('fds__getBankingtransfers_questionable' ,@authuser , '');
|
|
||||||
|
|
||||||
DECLARE @startdate date, @enddate date, @today date = GETDATE();
|
|
||||||
DECLARE @true bit = 1, @false bit = 0;
|
|
||||||
SET @mode = LOWER(@mode);
|
|
||||||
|
|
||||||
DECLARE @invidlist [dbo].[fds__tt__idlist_vchar];
|
|
||||||
|
|
||||||
--output admin
|
|
||||||
SELECT [startdate] = @startdate, [enddate] = @enddate, [mode] = @mode, [title] = 'Zahlungen (neueste zuerst)'
|
|
||||||
,[note] = 'Letzes Buchungsdatum: ' + FORMAT((SELECT MAX([valuedate]) from [fds__bankingtransactions]), 'dd.MM.yy');
|
|
||||||
|
|
||||||
|
|
||||||
select
|
|
||||||
ic.[taID]
|
|
||||||
--inv.InvoiceId,
|
|
||||||
--[IsPayed],
|
|
||||||
, ic.fds
|
|
||||||
, InvoiceId = ISNULL(fi.[invoiceid], mi.[invoiceid])
|
|
||||||
, [ValueDate] = FORMAT(ic.[ValueDate], 'dd.MM.yy')
|
|
||||||
, [InvoiceBalance] = FORMAT((ISNULL(fi.[InvoiceBalance],0.0) + ISNULL(mi.[InvoiceBalance],0.0)), '0.00€', 'de')
|
|
||||||
, [Amount] = FORMAT(ic.[amount], '0.00€', 'de')
|
|
||||||
, [Skonto] = CAST(CASE WHEN ic.[amount] between (0.965 * (ISNULL(fi.[InvoiceBalance],0.0) + ISNULL(mi.[InvoiceBalance],0.0))) and (0.975 * (ISNULL(fi.[InvoiceBalance],0.0) + ISNULL(mi.[InvoiceBalance],0.0))) THEN 1 ELSE 0 END as bit)
|
|
||||||
, [Deviation] = FORMAT(CASE WHEN ISNULL((ISNULL(fi.[InvoiceBalance],0.0) + ISNULL(mi.[InvoiceBalance],0.0)),0) > 9 THEN ic.[amount] / (ISNULL(fi.[InvoiceBalance],0.0) + ISNULL(mi.[InvoiceBalance],0.0)) ELSE NULL END , '0.00%')
|
|
||||||
--, ic.*
|
|
||||||
,ic.AccountNumberOfPayer
|
|
||||||
,ic.NameOfPayer
|
|
||||||
,ic.SepaRemittanceInformation
|
|
||||||
,ic.EndToEndReference
|
|
||||||
, [order] = ROW_NUMBER() OVER (ORDER BY [valuedate] DESC)
|
|
||||||
FROM
|
|
||||||
[dbo].[fds__fn_bankingtransactions](null, null, @startdate, @enddate, @invidlist) as ic
|
|
||||||
LEFT JOIN [dbo].[fds__invoices] as fi on ic.[fds] = 1 and ic.[invid] = fi.id
|
|
||||||
LEFT JOIN [dbo].[mfr__invoices] as mi on ic.[fds] = 0 and ic.[invid] = mi.id
|
|
||||||
END
|
|
||||||
-60
@@ -1,60 +0,0 @@
|
|||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__getBankingtransfers_questionable]
|
|
||||||
@mode varchar(1) = 'm'
|
|
||||||
, @authuser varchar(100)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
IF [dbo].[fis_getModuleAuth]('fds_bam', @authuser) < 1
|
|
||||||
THROW 60000, N'not authorized', 1;
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info])
|
|
||||||
VALUES ('fds__getBankingtransfers_questionable' ,@authuser , '');
|
|
||||||
|
|
||||||
DECLARE @startdate date, @enddate date, @today date = GETDATE();
|
|
||||||
DECLARE @true bit = 1, @false bit = 0;
|
|
||||||
SET @mode = LOWER(@mode);
|
|
||||||
|
|
||||||
|
|
||||||
--output admin
|
|
||||||
SELECT [startdate] = @startdate, [enddate] = @enddate, [mode] = @mode, [title] = 'Auffällige Zahlungen (neueste zuerst)'
|
|
||||||
,[note] = 'Letzes Buchungsdatum: ' + FORMAT((SELECT MAX([valuedate]) from [fds__bankingtransactions]), 'dd.MM.yy');
|
|
||||||
|
|
||||||
|
|
||||||
with inv as (
|
|
||||||
SELECT [fds] = CAST(1 as bit), [InvId] = CAST([Id] as varchar(25)), [InvoiceId], [InvoiceBalance] = CAST([InvoiceBalance] as numeric(10,2)), [ispayed] FROM [dbo].[fds__invoices] as i
|
|
||||||
UNION
|
|
||||||
SELECT [fds] = CAST(0 as bit), [InvId] = CAST([Id] as varchar(25)), [InvoiceId], [InvoiceBalance] = CAST([InvoiceBalance] as numeric(10,2)), [ispayed] = CAST(CASE WHEN [InvoiceState] = 'eIsCancelled' THEN 1 WHEN [InvoiceState] = 'eIsPaid' THEN 1 ELSE 0 END as bit) FROM [dbo].[mfr__invoices] as i
|
|
||||||
)
|
|
||||||
select
|
|
||||||
ic.[taID]
|
|
||||||
--inv.InvoiceId,
|
|
||||||
--[IsPayed],
|
|
||||||
, inv.fds
|
|
||||||
, ic.InvoiceId
|
|
||||||
, [ValueDate] = FORMAT(ic.[ValueDate], 'dd.MM.yy')
|
|
||||||
, [InvoiceBalance] = FORMAT(inv.[InvoiceBalance], '0.00€', 'de') + ISNULL( CASE WHEN id2.[invid] is not null then (SELECT TOP(1) FORMAT(inv2.[InvoiceBalance], '0.00€', 'de') FROM [dbo].[fds__invoices] as inv2 where inv2.Id = id2.[invid]) ELSE '' END ,'')
|
|
||||||
, [Amount] = FORMAT(ic.[amount], '0.00€', 'de')
|
|
||||||
, [Skonto] = CAST(CASE WHEN ic.[amount] between (0.965 * inv.[InvoiceBalance]) and (0.975 * inv.[InvoiceBalance]) THEN 1 ELSE 0 END as bit)
|
|
||||||
, [Deviation] = FORMAT(CASE WHEN ISNULL(inv.[InvoiceBalance],0) > 9 THEN ic.[amount] / inv.[InvoiceBalance] ELSE NULL END , '0.00%')
|
|
||||||
--, ic.*
|
|
||||||
,ic.AccountNumberOfPayer
|
|
||||||
,ic.NameOfPayer
|
|
||||||
,ic.SepaRemittanceInformation
|
|
||||||
,ic.EndToEndReference
|
|
||||||
, [order] = ROW_NUMBER() OVER (ORDER BY [valuedate] DESC)
|
|
||||||
FROM
|
|
||||||
[dbo].[fds__getInvoiceCredits] (null, null) as ic
|
|
||||||
LEFT JOIN inv on inv.[InvoiceId] = ic.[invoiceid]
|
|
||||||
LEFT JOIN [dbo].[fds__invoice_details] as id2 ON inv.InvId = id2.[stornoto_invid]
|
|
||||||
WHERE
|
|
||||||
((ISNULL(inv.ispayed,0) = 0 and fds = 1) OR
|
|
||||||
ic.[amount] not between (inv.InvoiceBalance - 0.99) and (inv.InvoiceBalance + 0.99) OR
|
|
||||||
ic.invoiceid is null oR
|
|
||||||
inv.invoiceid is null
|
|
||||||
) AND NOT EXISTS (SELECT 0 FROM [dbo].[fds__bankingtransactions_settings] as bs WHERE bs.[taID] = ic.[taID] AND (ISNULL(bs.[done_manually],'') <> '' OR ISNULL(bs.[assigned_invoice_id],'') <> ''))
|
|
||||||
END
|
|
||||||
@@ -1,634 +0,0 @@
|
|||||||
|
|
||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__getDatevExports]
|
|
||||||
@tgtdate date
|
|
||||||
,@mode varchar(1) = 'm'
|
|
||||||
,@files bit
|
|
||||||
,@authuser varchar(100)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
|
|
||||||
IF [dbo].[fis_getModuleAuth]('fds_reports', @authuser) < 2
|
|
||||||
THROW 60000, N'not authorized', 1;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @startdate date, @enddate date;
|
|
||||||
SET @mode = LOWER(@mode);
|
|
||||||
If @mode = 'm'
|
|
||||||
BEGIN
|
|
||||||
SELECT @startdate = [dbo].[date_monthfirst](@tgtdate)
|
|
||||||
,@enddate = [dbo].[date_monthend](@tgtdate);
|
|
||||||
END
|
|
||||||
ELSE If @mode = 'w'
|
|
||||||
BEGIN
|
|
||||||
SELECT @startdate = [dbo].[date_weekfirst](@tgtdate)
|
|
||||||
,@enddate = [dbo].[date_weekend](@tgtdate);
|
|
||||||
END
|
|
||||||
|
|
||||||
DECLARE @contraaccounts as table([account] varchar(10), [vat] numeric(5,2));
|
|
||||||
insert into @contraaccounts
|
|
||||||
SELECT [account] = [key], [vat] = TRY_PARSE([value2] as numeric(5,2) USING 'en-US') FROM [dbo].[fds__admin_settings] WHERE [type] = 'contra_account'
|
|
||||||
|
|
||||||
SELECT [startdate] = @startdate, [enddate] = @enddate, [mode] = @mode
|
|
||||||
, [beraternummer] = 11653
|
|
||||||
, [mandantennummer] = 62321
|
|
||||||
, [WJ-Beginn] = DATEFROMPARTS(YEAR(@tgtDate), 1, 1)
|
|
||||||
, [Sachkontenlänge] = ISNULL((SELECT MAX(LEN([account])) FROM @contraaccounts), 4)
|
|
||||||
;
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @inv TABLE([Id] varchar(20), [InvoiceID] varchar(255), [URI] nvarchar(255), [DocumentName] nvarchar(255), [DateOfCreation] date, [fds] bit, [file_guid] varchar(36));
|
|
||||||
--INSERT INTO @inv SELECT [Id],[invoiceid],[URI],[DocumentName],[DateOfCreation],[fds] = 0 FROM [dbo].[mfr__reports] where [IsInvoice] = 1 AND CAST([DateOfCreation] as date) between @startdate AND @enddate;
|
|
||||||
INSERT INTO @inv SELECT [Id],[invoiceid],[URI],[DocumentName],[DateOfCreation],[fds] = 0, [file_guid] FROM [dbo].[mfr__invoices] where CAST([DateOfCreation] as date) between @startdate AND @enddate and [FileType] in( 'PdfInvoice','PdfCancelInvoice','PdfPartialInvoice') and ISNULL(InvoiceId,'') not in ('Preview','') ;
|
|
||||||
INSERT INTO @inv SELECT [Id],[invoiceid],[URI] = null,[DocumentName],[DateOfCreation] = [DateCreated],[fds] = 1, [file_guid] FROM [dbo].[fds__invoices] where CAST([DateCreated] as date) between @startdate AND @enddate and [isfinal] = 1;
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
inv.[Id],inv.[InvoiceID],inv.[URI],inv.[DocumentName],inv.[fds]
|
|
||||||
, [file] = CASE WHEN @files = 1 then finv.[file] ELSE NULL END
|
|
||||||
, inv.[file_guid], [DateOfCreation]
|
|
||||||
--,[order] = ROW_NUMBER() OVER (ORDER BY [DateOfCreation] ASC)
|
|
||||||
FROM @inv as inv LEFT JOIN [dbo].[fds__invoices] as finv on inv.[id] = finv.[id];
|
|
||||||
|
|
||||||
DECLARE @SReq TABLE ([id] bigint, [DateOfCreation] datetime, [name] nvarchar(255), [invoice.Id] varchar(15), [customer.ExternalId] bigint, [customer.Id] bigint, [ExternalId] nvarchar(255), [fds] bit);
|
|
||||||
INSERT INTO @SReq
|
|
||||||
SELECT s.[Id], s.[DateOfCreation], s.[name], [invoice.Id] = i.[Id], [customer.ExternalId] = cy.ExternalId , [customer.Id] = cy.[id], s.[ExternalId], [fds]
|
|
||||||
FROM [dbo].[mfr__servicerequests] as s JOIN @inv as i on s.[InvoiceId] like ('%' + i.[InvoiceId] + '%') LEFT JOIN [dbo].[mfr__companies] as cy on s.[CustomerId] = cy.id
|
|
||||||
WHERE i.fds = 0;
|
|
||||||
INSERT INTO @SReq
|
|
||||||
SELECT s.[Id], s.[DateOfCreation], s.[name], [invoice.Id] = i.[Id], [customer.ExternalId] = cy.ExternalId , [customer.Id] = cy.[id], s.[ExternalId], [fds]
|
|
||||||
FROM [dbo].[fds__invoice_servicerequests] as irq
|
|
||||||
JOIN @inv as i on irq.[InvId] = i.[Id]
|
|
||||||
LEFT JOIN [dbo].[mfr__servicerequests] as s ON irq.[mfr__servicerequest] = s.[id]
|
|
||||||
LEFT JOIN [dbo].[mfr__companies] as cy on s.[CustomerId] = cy.id
|
|
||||||
WHERE i.fds = 1;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
With itm as (
|
|
||||||
SELECT
|
|
||||||
[net_itemsum] = CAST( SUM((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) as numeric(12,2))
|
|
||||||
, [bo_itemsum] = CAST( SUM( ((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) * (1 + (ISNULL(vat.[vat], 19.0) * 0.01))) as numeric(12,2))
|
|
||||||
, [vf] = MAX(1 + (ISNULL(vat.[vat], 19.0) * 0.01))
|
|
||||||
, sreq.[invoice.Id]
|
|
||||||
,[fds] = 0
|
|
||||||
FROM [dbo].[mfr__items] as itm
|
|
||||||
JOIN @sreq as sreq ON itm.[ServiceRequestId] = SReq.[id]
|
|
||||||
LEFT JOIN [dbo].[fds__custom_vat] as vat ON itm.[Id] = vat.[EntityId] and vat.[EntityType] = 'item'
|
|
||||||
WHERE [fds] = 0
|
|
||||||
GROUP BY sreq.[invoice.Id]
|
|
||||||
UNION
|
|
||||||
SELECT
|
|
||||||
[net_itemsum] = CAST( SUM(ISNULL([value_total],0)) as numeric(12,2))
|
|
||||||
, [bo_itemsum] = CAST( SUM(ISNULL([value_total],0) * (1 + (ISNULL([vat], 19.0) * 0.01))) as numeric(12,2))
|
|
||||||
, [vf] = MAX(1 + (ISNULL([vat], 19.0) * 0.01))
|
|
||||||
, [invoice.Id] = inv.[id] --sreq.[invoice.Id]
|
|
||||||
, [fds] = 1
|
|
||||||
FROM [dbo].[fds__invoice_items] as itm
|
|
||||||
--JOIN @sreq as sreq ON itm.[InvRqId] = SReq.[id]
|
|
||||||
JOIN @inv as inv ON itm.[invid] = inv.[id] and [fds] = 1
|
|
||||||
WHERE [fds] = 1
|
|
||||||
GROUP BY inv.[id] --sreq.[invoice.Id]
|
|
||||||
),sc as(
|
|
||||||
SELECT s.[invoice.Id], [fds], [#] = COUNT(s.[id]) FROM @SReq as s GROUP BY s.[invoice.Id], [fds]
|
|
||||||
), inv as(
|
|
||||||
--select
|
|
||||||
-- [Id] = CAST(iv.[Id] as varchar(20))
|
|
||||||
-- ,[InvoiceId] = iv.[InvoiceId]
|
|
||||||
-- ,[requestcount] = sc.[#]
|
|
||||||
-- ,[request] = (SELECT TOP(1) s.[ExternalId] FROM @SReq as s WHERE s.[invoice.Id] = iv.id)
|
|
||||||
-- ,[balance] = ABS([invoiceBalance])
|
|
||||||
-- ,[EINZELPOS_brutto] = itm.bo_itemsum
|
|
||||||
-- ,[EINZELPOS_netto] = itm.net_itemsum
|
|
||||||
-- ,[vf]
|
|
||||||
-- ,[deb_cred] = CASE WHEN [invoiceBalance] >= 0 THEN 'S' ELSE 'H' END
|
|
||||||
-- ,[customer] = (SELECT TOP(1) [customer.ExternalId] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id] and [customer.ExternalId] is not null ORDER BY SReq.DateOfCreation ASC)
|
|
||||||
-- ,[contra_account] = ca.[account]
|
|
||||||
-- ,[Belegdatum] = iv.[DateOfCreation]
|
|
||||||
-- ,[Buchungstext] = (SELECT TOP(1) SReq.[name] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id])
|
|
||||||
-- ,iv.[DateOfCreation]
|
|
||||||
-- from [dbo].[mfr__reports] as iv
|
|
||||||
-- LEFT JOIN itm on iv.Id = itm.[invoice.Id]
|
|
||||||
-- LEFT JOIN [dbo].[fds__custom_vat] as vat ON iv.[Id] = vat.[EntityId] and vat.[EntityType] = 'report'
|
|
||||||
-- LEFT JOIN sc on iv.[id] = sc.[invoice.Id] and sc.[fds] = 0
|
|
||||||
-- LEFT JOIN contra_accounts as ca ON ISNULL(vat.[vat], 19.0) = ca.[vat]
|
|
||||||
-- where iv.Id in (SELECT [Id] FROM @inv Where fds = 0)
|
|
||||||
--UNION
|
|
||||||
select
|
|
||||||
[Id] = CAST(iv.[Id] as varchar(20))
|
|
||||||
,[InvoiceId] = iv.[InvoiceId]
|
|
||||||
,[requestcount] = sc.[#]
|
|
||||||
,[request] = (SELECT TOP(1) s.[ExternalId] FROM @SReq as s WHERE s.[invoice.Id] = iv.id)
|
|
||||||
,[balance] = ABS([invoiceBalance])
|
|
||||||
,[EINZELPOS_brutto] = itm.bo_itemsum
|
|
||||||
,[EINZELPOS_netto] = itm.net_itemsum
|
|
||||||
,[vf]
|
|
||||||
,[deb_cred] = CASE WHEN [invoiceBalance] >= 0 THEN 'S' ELSE 'H' END
|
|
||||||
,[customer] = (SELECT TOP(1) [customer.ExternalId] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id] and [customer.ExternalId] is not null ORDER BY SReq.DateOfCreation ASC)
|
|
||||||
,[contra_account] = ca.[account]
|
|
||||||
,[Belegdatum] = iv.[DateOfCreation]
|
|
||||||
,[Buchungstext] = (SELECT TOP(1) SReq.[name] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id])
|
|
||||||
,iv.[DateOfCreation]
|
|
||||||
,[file_guid]
|
|
||||||
from [dbo].[mfr__invoices] as iv
|
|
||||||
LEFT JOIN itm on iv.Id = itm.[invoice.Id]
|
|
||||||
LEFT JOIN [dbo].[fds__custom_vat] as vat ON iv.[Id] = vat.[EntityId] and vat.[EntityType] = 'report'
|
|
||||||
LEFT JOIN sc on iv.[id] = sc.[invoice.Id] and sc.[fds] = 0
|
|
||||||
LEFT JOIN @contraaccounts as ca ON ISNULL(vat.[vat], ISNULL([dbo].[fds__fn_vatfrombalances]([invoiceBalance], [invoiceBalancenetto]),19.0)) = ca.[vat]
|
|
||||||
where iv.Id in (SELECT [Id] FROM @inv Where fds = 0)
|
|
||||||
UNION
|
|
||||||
select
|
|
||||||
iv.[Id]
|
|
||||||
,[InvoiceId] = iv.[InvoiceId]
|
|
||||||
,[requestcount] = sc.[#]
|
|
||||||
,[request] = (SELECT TOP(1) s.[ExternalId] FROM @SReq as s WHERE s.[invoice.Id] = iv.id)
|
|
||||||
,[balance] = ABS([invoiceBalance])
|
|
||||||
,[EINZELPOS_brutto] = itm.bo_itemsum
|
|
||||||
,[EINZELPOS_netto] = itm.net_itemsum
|
|
||||||
,[vf]
|
|
||||||
,[deb_cred] = CASE WHEN [invoiceBalance] >= 0 THEN 'S' ELSE 'H' END
|
|
||||||
,[customer] = (SELECT TOP(1) [ExternalId] FROM [dbo].[mfr__companies] as c WHERE c.[Id] = iv.[CustomerId])
|
|
||||||
,[contra_account] = ca.[account]
|
|
||||||
,[Belegdatum] = iv.[DateCreated]
|
|
||||||
,[Buchungstext] = (SELECT TOP(1) SReq.[name] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id])
|
|
||||||
,[DateOfCreation] = iv.[DateCreated]
|
|
||||||
,[file_guid]
|
|
||||||
from [dbo].[fds__invoices] as iv
|
|
||||||
LEFT JOIN itm on iv.Id = itm.[invoice.Id]
|
|
||||||
LEFT JOIN sc on iv.[id] = sc.[invoice.Id] and sc.[fds] = 1
|
|
||||||
LEFT JOIN @contraaccounts as ca ON ISNULL(iv.[InvoiceVAT_1], 19.0) = ca.[vat]
|
|
||||||
where iv.Id in (SELECT [Id] FROM @inv Where fds = 1)
|
|
||||||
)
|
|
||||||
select
|
|
||||||
[Umsatz (ohne Soll/Haben-Kz)] = ABS([balance])--CAST(FORMAT(ABS([balance]), '0.00', 'de') as varchar(13))
|
|
||||||
--,[EINZELPOS_brutto]
|
|
||||||
--,[EINZELPOS_netto]
|
|
||||||
--,vf
|
|
||||||
,[Soll/Haben-Kennzeichen] = [dbo].[ott_quote_255](UPPER([deb_cred]))
|
|
||||||
,[WKZ Umsatz] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Kurs] = NULL
|
|
||||||
,[Basis-Umsatz] = NULL
|
|
||||||
,[WKZ Basis-Umsatz] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Konto] = CAST(iv.[customer] as bigint) --CAST(iv.[customer] as varchar(9)) --(SELECT TOP(1) [customer.ExternalId] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id] ORDER BY SReq.DateOfCreation ASC)
|
|
||||||
,[Gegenkonto (ohne BU-Schlüssel)] = CAST([contra_account] as bigint )--CAST([contra_account] as varchar(9))]
|
|
||||||
,[BU-Schlüssel] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Belegdatum] = FORMAT(iv.[Belegdatum], 'ddMM')
|
|
||||||
,[Belegfeld 1] = [dbo].[ott_quote_255](iv.[InvoiceId])
|
|
||||||
,[Belegfeld 2] = [dbo].[ott_quote_255]([dbo].[strings_removeLeading0_255](FORMAT(iv.[Belegdatum], 'ddMMyy')))
|
|
||||||
,[Skonto] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Buchungstext] = [dbo].[ott_quote_255](CAST(ISNULL(LEFT( TRIM(CAST((SELECT TOP(1) SReq.[name] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id]) as varchar(255)) ), 60) ,'') as varchar(60)))
|
|
||||||
,[Postensperre] = NULL
|
|
||||||
,[Diverse Adressnummer] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Geschäftspartnerbank] = null
|
|
||||||
,[Sachverhalt] = null
|
|
||||||
,[Zinssperre] = null
|
|
||||||
,[Beleglink] = [dbo].[ott_quote_255](CASE WHEN [file_guid] is null THEN NULL
|
|
||||||
ELSE CAST( 'BEDI "' + LOWER(CAST([file_guid] as varchar(36))) + '"' as varchar(50))
|
|
||||||
end)
|
|
||||||
--,[order] = ROW_NUMBER() OVER (ORDER BY iv.[DateOfCreation] ASC)]
|
|
||||||
,[Beleginfo - Art 1] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Beleginfo - Inhalt 1] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Beleginfo - Art 2] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Beleginfo - Inhalt 2] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Beleginfo - Art 3] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Beleginfo - Inhalt 3] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Beleginfo - Art 4] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Beleginfo - Inhalt 4] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Beleginfo - Art 5] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Beleginfo - Inhalt 5] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Beleginfo - Art 6] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Beleginfo - Inhalt 6] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Beleginfo - Art 7] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Beleginfo - Inhalt 7] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Beleginfo - Art 8] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Beleginfo - Inhalt 8] = [dbo].[ott_quote_255]('')
|
|
||||||
,[KOST1 - Kostenstelle] = [dbo].[ott_quote_255]('')
|
|
||||||
,[KOST2 - Kostenstelle] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Kost-Menge] = null
|
|
||||||
,[EU-Land u. UStID (Bestimmung)] = [dbo].[ott_quote_255]('')
|
|
||||||
,[EU-Steuersatz (Bestimmung)] = null
|
|
||||||
,[Abw. Versteuerungsart] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Sachverhalt L+L] = null
|
|
||||||
,[Funktionsergänzung L+L] = null
|
|
||||||
,[BU 49 Hauptfunktionstyp] = null
|
|
||||||
,[BU 49 Hauptfunktionsnummer] = null
|
|
||||||
,[BU 49 Funktionsergänzung] = null
|
|
||||||
,[Zusatzinformation - Art 1] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation- Inhalt 1] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation - Art 2] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation- Inhalt 2] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation - Art 3] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation- Inhalt 3] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation - Art 4] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation- Inhalt 4] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation - Art 5] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation- Inhalt 5] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation - Art 6] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation- Inhalt 6] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation - Art 7] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation- Inhalt 7] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation - Art 8] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation- Inhalt 8] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation - Art 9] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation- Inhalt 9] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation - Art 10] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation- Inhalt 10] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation - Art 11] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation- Inhalt 11] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation - Art 12] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation- Inhalt 12] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation - Art 13] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation- Inhalt 13] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation - Art 14] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation- Inhalt 14] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation - Art 15] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation- Inhalt 15] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation - Art 16] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation- Inhalt 16] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation - Art 17] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation- Inhalt 17] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation - Art 18] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation- Inhalt 18] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation - Art 19] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation- Inhalt 19] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation - Art 20] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zusatzinformation- Inhalt 20] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Stück] = null
|
|
||||||
,[Gewicht] = null
|
|
||||||
,[Zahlweise] = null
|
|
||||||
,[Forderungsart] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Veranlagungsjahr] = null
|
|
||||||
,[Zugeordnete Fälligkeit] = null
|
|
||||||
,[Skontotyp] = null
|
|
||||||
,[Auftragsnummer] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Buchungstyp] = [dbo].[ott_quote_255]('')
|
|
||||||
,[USt-Schlüssel (Anzahlungen)] = null
|
|
||||||
,[EU-Land (Anzahlungen)] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Sachverhalt L+L (Anzahlungen)] = null
|
|
||||||
,[EU-Steuersatz (Anzahlungen)] = null
|
|
||||||
,[Erlöskonto (Anzahlungen)] = null
|
|
||||||
,[Herkunft-Kz] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Buchungs GUID] = [dbo].[ott_quote_255]('')
|
|
||||||
,[KOST-Datum] = null
|
|
||||||
,[Mandatsreferenz] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Skontosperre] = null
|
|
||||||
,[Gesellschaftername] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Beteiligtennummer] = null
|
|
||||||
,[Identifikationsnummer] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Zeichnernummer] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Postensperre bis] = null
|
|
||||||
,[Bezeichnung SoBil-Sachverhalt] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Kennzeichen SoBil-Buchung] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Festschreibung] = 1
|
|
||||||
,[Leistungsdatum] = null
|
|
||||||
,[Datum Zuord. Steuerperiode] = null
|
|
||||||
,[Fälligkeit] = null
|
|
||||||
,[Generalumkehr (GU)] = 0
|
|
||||||
,[Steuersatz] = null
|
|
||||||
,[Land] = null
|
|
||||||
--,[Abrechnungsreferenz]
|
|
||||||
--,[BVV-Position]
|
|
||||||
--,[EU-Land u. UStID (Ursprung)]
|
|
||||||
--,[EU-Steuersatz (Ursprung)]
|
|
||||||
from inv as iv
|
|
||||||
order by TRY_CONVERT(integer, SUBSTRING(iv.[InvoiceId], 2,4)), TRY_CONVERT(integer, RIGHT(iv.[InvoiceId], 4)), iv.[DateOfCreation] ASC;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
WIth debs as (
|
|
||||||
SELECT ROW_NUMBER() OVER (PARTITION BY [customer.Id] ORDER BY SReq.DateOfCreation ASC) as [#]
|
|
||||||
, [customer.ExternalId]
|
|
||||||
, [customer.Id]
|
|
||||||
FROM @SReq as sreq
|
|
||||||
), dd as (
|
|
||||||
SELECT distinct cy.* FROM [mfr__companies] as cy WHERE EXISTS ( SELECT * FROM debs WHERE debs.[customer.Id] = cy.[id] AND debs.[#] = 1)
|
|
||||||
), cc as (
|
|
||||||
SELECT ROW_NUMBER() OVER (PARTITION BY dd.[id] ORDER BY CASE WHEN dd.[MainContactId] = c.[id] THEN 0 ELSE 1 END) as '#'
|
|
||||||
, c.*
|
|
||||||
, [company_id] = dd.[id]
|
|
||||||
, [company_name] = dd.[Name]
|
|
||||||
, [isCompany] = CAST (
|
|
||||||
CASE WHEN ( not dd.[name] like '%c[/]o %'
|
|
||||||
AND (
|
|
||||||
dd.[Name] like '%mbH%'
|
|
||||||
OR dd.[Name] like '%Ltd%'
|
|
||||||
OR dd.[Name] like '% gbr%' OR dd.[Name] like 'gbr %'
|
|
||||||
OR dd.[Name] like '% OHG%'
|
|
||||||
OR (REPLACE(dd.[Name], '|' , ' ') + ' ') like '% AG %'
|
|
||||||
OR (REPLACE(dd.[Name], '|' , ' ') + ' ') like '% KG %'
|
|
||||||
OR (REPLACE(dd.[Name], '|' , ' ') + ' ') like '% SA %'
|
|
||||||
OR (REPLACE(dd.[Name], '|' , ' ') + ' ') like '% UG %'
|
|
||||||
OR dd.[Name] like '% eV%' or dd.[Name] like '%e[.]V[.]%'
|
|
||||||
OR dd.[Name] like '%Gesellschaft%'
|
|
||||||
OR dd.[Name] like '%Gemeinde%'
|
|
||||||
OR dd.[Name] like '%Verwaltung%'
|
|
||||||
OR dd.[Name] like '%Stadtwerke%'
|
|
||||||
OR dd.[Name] like '%tagesstätte%'
|
|
||||||
OR dd.[Name] like '%schützenverein%'
|
|
||||||
OR dd.[Name] like '%akademie%'
|
|
||||||
OR dd.[Name] like '%universität%'
|
|
||||||
OR dd.[Name] like '%schule%'
|
|
||||||
OR dd.[Name] like '%spedition%'
|
|
||||||
OR dd.[Name] like '%immobilien%'
|
|
||||||
OR dd.[Name] like '%krankenkasse%'
|
|
||||||
OR dd.[Name] like '% holding %'
|
|
||||||
OR dd.[Name] like '% Bautenschutz %'
|
|
||||||
OR dd.[Name] like '%dienstleistung%'
|
|
||||||
OR dd.[Name] like '%weinlokal%'
|
|
||||||
OR dd.[Name] like '%restaurant%'
|
|
||||||
OR dd.[Name] like '% hotel%'
|
|
||||||
OR dd.[Name] like '%kanzlei%'
|
|
||||||
OR dd.[Name] like '%praxis%'
|
|
||||||
OR dd.[Name] like '%großhandel%'
|
|
||||||
OR dd.[Name] like '%tischlerei%'
|
|
||||||
OR dd.[Name] like '%haustechnik%'
|
|
||||||
OR dd.[Name] like '%holztechnik%'
|
|
||||||
OR dd.[Name] like '%heizungsbau%'
|
|
||||||
OR dd.[Name] like '%bautenschutz%'
|
|
||||||
OR dd.[Name] like '%tischlerei%'
|
|
||||||
OR dd.[Name] like '%architekt%'
|
|
||||||
OR dd.[Name] like '%sanitär%'
|
|
||||||
OR dd.[Name] like '%pafümerie%'
|
|
||||||
OR dd.[Name] like '%bauamt%'
|
|
||||||
OR dd.[Name] like '%gebäudereinigung%'
|
|
||||||
OR dd.[Name] like '%handwerksmeister%'
|
|
||||||
OR dd.[Name] like '%fachbetrieb%'
|
|
||||||
OR dd.[Name] like '%ausbau und montagen%'
|
|
||||||
OR dd.[Name] like '%verband %'
|
|
||||||
OR dd.[Name] like '%vermietung%'
|
|
||||||
OR dd.[Name] like '%Bad [&] Heizung%'
|
|
||||||
OR dd.[Name] like '%Bad[&]Wärme%'
|
|
||||||
OR dd.[Name] like '% Düsseldorf%'
|
|
||||||
OR dd.[Name] like '%niederlassung%'
|
|
||||||
OR dd.[Name] like 'zentrum für %'
|
|
||||||
OR dd.[Name] like '% Generalvertretung %'
|
|
||||||
))
|
|
||||||
|
|
||||||
THEN 1 ELSE 0 END
|
|
||||||
as bit)
|
|
||||||
FROM [dbo].[mfr__contacts] as c JOIN dd on c.[CompanyId] = dd.[Id] OR dd.[MainContactId] = c.[id]
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
[Konto] = CAST(dd.[ExternalId] as bigint) --CAST(dd.[ExternalId] as varchar(9))
|
|
||||||
,[Name (Adressatentyp Unternehmen)] = [dbo].[ott_quote_255](CAST(LEFT(CAST(CASE WHEN cc.[isCompany] = 0 THEN '' ELSE REPLACE(REPLACE(REPLACE(ISNULL( cc.[company_name] ,''), CHAR(13), ' '), CHAR(10), ' '), ' ', ' ') END as varchar(255)), 50) as varchar(50)))
|
|
||||||
,[Unternehmensgegenstand] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Name (Adressattyp natürl. Person)] = [dbo].[ott_quote_255](CAST(LEFT(CAST(CASE WHEN cc.[isCompany] = 1 THEN '' ELSE REPLACE(REPLACE(REPLACE(ISNULL( cc.[LastName] ,''), CHAR(13), ' '), CHAR(10), ' '), ' ', ' ') END as varchar(255)), 30) as varchar(30)))
|
|
||||||
,[Vorname (Adressattyp natürl. Person)] = [dbo].[ott_quote_255](CAST(LEFT(CAST(CASE WHEN cc.[isCompany] = 1 THEN '' ELSE REPLACE(REPLACE(REPLACE(ISNULL( cc.[FirstName] ,''), CHAR(13), ' '), CHAR(10), ' '), ' ', ' ') END as varchar(255)), 30) as varchar(30)))
|
|
||||||
,[Name (Adressattyp keine Angabe)] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Adressattyp] = [dbo].[ott_quote_255](CAST(CASE WHEN cc.[isCompany] = 1 THEN 2 ELSE 1 END as varchar(1)))
|
|
||||||
,[Kurzbezeichnung] = [dbo].[ott_quote_255]('')
|
|
||||||
,[EU-Land] = [dbo].[ott_quote_255]('')
|
|
||||||
,[EU-UStID] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Anrede] = [dbo].[ott_quote_255]('') --CASE WHEN LOWER(LEFT(cc.[Gender],1)) = 'm' THEN 'Herrn' WHEN LOWER(LEFT(cc.[Gender],1)) = 'w' THEN 'Frau' ELSE '' END
|
|
||||||
,[Titel/Akad. Grad] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Adelstitel] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Namensvorsatz] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Adressart] = [dbo].[ott_quote_255](CAST( CASE WHEN ISNULL(l.[AddressString],'') like '%postfach%' THEN 'PF' ELSE 'STR' END as varchar(3)))
|
|
||||||
,[Straße] = [dbo].[ott_quote_255](CAST(LEFT(CAST(CASE WHEN ISNULL(l.[AddressString],'') like '%postfach%' THEN '' ELSE ISNULL(l.[AddressString],'') END as varchar(255)),36) as varchar(36)))
|
|
||||||
,[Postfach] = [dbo].[ott_quote_255](CAST(LEFT(CAST(CASE WHEN ISNULL(l.[AddressString],'') like '%postfach%' THEN TRIM(SUBSTRING(ISNULL(l.[AddressString],''), PATINDEX('%postfach%', ISNULL(l.[AddressString],'')) + 8 , 12)) ELSE '' END as varchar(255)),36) as varchar(36)))
|
|
||||||
,[Postleitzahl] = [dbo].[ott_quote_255](CAST(l.[Postal] as varchar(10)))
|
|
||||||
,[Ort] = [dbo].[ott_quote_255](CAST(l.City as varchar(30)))
|
|
||||||
,[Land] = [dbo].[ott_quote_255](CAST(REPLACE(UPPER(ISNULL(l.[Country], '')),'KE','') as varchar(2)))
|
|
||||||
,[Versandzusatz] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Adresszusatz] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Abweichende Anrede] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Abw. Zustellbezeichnung 1] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Abw. Zustellbezeichnung 2] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Kennz. Korrespondenzadresse] = Cast(1 as int)
|
|
||||||
,[Adresse Gültig von] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Adresse Gültig bis] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Telefon] = [dbo].[ott_quote_255](CAST(ISNULL(cc.[Telephone], dd.[SupportTelephone]) as varchar(60)))
|
|
||||||
,[Bemerkung (Telefon)] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Telefon GL] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bemerkung (Telefon GL)] = [dbo].[ott_quote_255]('')
|
|
||||||
,[E-Mail] = [dbo].[ott_quote_255](CAST( CASE WHEN ISNULL(cc.[Email], dd.[SupportMail]) like '%[@]%' THEN REPLACE(LOWER(ISNULL(cc.[Email], dd.[SupportMail])), ' ', '') ELSE NULL END as varchar(60)))
|
|
||||||
,[Bemerkung (E-Mail)] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Internet] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bemerkung (Internet)] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Fax] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bemerkung (Fax)] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Sonstige] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bemerkung (Sonstige)] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bankleitzahl 1] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bankbezeichnung 1] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bank-Kontonummer 1] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Länderkennzeichen 1] = [dbo].[ott_quote_255]('')
|
|
||||||
,[IBAN-Nr. 1] = [dbo].[ott_quote_255]('')
|
|
||||||
,[IBAN1 korrekt] = [dbo].[ott_quote_255]('')
|
|
||||||
,[SWIFT-Code 1] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Abw. Kontoinhaber 1] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Kennz. Hauptbankverb. 1] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bankverb 1 Gültig von] = null
|
|
||||||
,[Bankverb 1 Gültig bis] = null
|
|
||||||
,[Bankleitzahl 2] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bankbezeichnung 2] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bank-Kontonummer 2] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Länderkennzeichen 2] = [dbo].[ott_quote_255]('')
|
|
||||||
,[IBAN-Nr. 2] = [dbo].[ott_quote_255]('')
|
|
||||||
,[IBAN2 korrekt] = [dbo].[ott_quote_255]('')
|
|
||||||
,[SWIFT-Code 2] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Abw. Kontoinhaber 2] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Kennz. Hauptbankverb. 2] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bankverb 2 Gültig von] = null
|
|
||||||
,[Bankverb 2 Gltig bis] = null
|
|
||||||
,[Bankleitzahl 3] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bankbezeichnung 3] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bank-Kontonummer 3] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Länderkennzeichen 3] = [dbo].[ott_quote_255]('')
|
|
||||||
,[IBAN-Nr. 3] = [dbo].[ott_quote_255]('')
|
|
||||||
,[IBAN3 korrekt] = [dbo].[ott_quote_255]('')
|
|
||||||
,[SWIFT-Code 3] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Abw. Kontoinhaber 3] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Kennz. Hauptbankverb. 3] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bankverb 3 Gültig von] = null
|
|
||||||
,[Bankverb 3 Gültig bis] = null
|
|
||||||
,[Bankleitzahl 4] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bankbezeichnung 4] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bank-Kontonummer 4] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Länderkennzeichen 4] = [dbo].[ott_quote_255]('')
|
|
||||||
,[IBAN-Nr. 4] = [dbo].[ott_quote_255]('')
|
|
||||||
,[IBAN4 korrekt] = [dbo].[ott_quote_255]('')
|
|
||||||
,[SWIFT-Code 4] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Abw. Kontoinhaber 4] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Kennz. Hauptbankverb. 4] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bankverb 4 Gültig von] = null
|
|
||||||
,[Bankverb 4 Gültig bis] = null
|
|
||||||
,[Bankleitzahl 5] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bankbezeichnung 5] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bank-Kontonummer 5] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Länderkennzeichen 5] = [dbo].[ott_quote_255]('')
|
|
||||||
,[IBAN-Nr. 5] = [dbo].[ott_quote_255]('')
|
|
||||||
,[IBAN5 korrekt] = [dbo].[ott_quote_255]('')
|
|
||||||
,[SWIFT-Code 5] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Abw. Kontoinhaber 5] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Kennz. Hauptbankverb. 5] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bankverb 5 Gültig von] = null
|
|
||||||
,[Bankverb 5 Gültig bis] = null
|
|
||||||
,[Leerfeld] = null
|
|
||||||
,[Briefanrede] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Grußformel] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Kundennummer] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Steuernummer] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Sprache] = null
|
|
||||||
,[Ansprechpartner] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Vertreter] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Sachbearbeiter] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Diverse-Konto] = null
|
|
||||||
,[Ausgabeziel] = null
|
|
||||||
,[Währungssteuerung] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Kreditlimit (Debitor)] = null
|
|
||||||
,[Zahlungsbedingung] = null
|
|
||||||
,[Fälligkeit in Tagen (Debitor)] = null
|
|
||||||
,[Skonto in Prozent (Debitor)] = null
|
|
||||||
,[Kreditoren-Ziel 1 Tg.] = null
|
|
||||||
,[Kreditoren-Skonto 1 %] = null
|
|
||||||
,[Kreditoren-Ziel 2 Tg.] = null
|
|
||||||
,[Kreditoren-Skonto 2 %] = null
|
|
||||||
,[Kreditoren-Ziel 3 Brutto Tg.] = null
|
|
||||||
,[Kreditoren-Ziel 4 Tg.] = null
|
|
||||||
,[Kreditoren-Skonto 4 %] = null
|
|
||||||
,[Kreditoren-Ziel 5 Tg.] = null
|
|
||||||
,[Kreditoren-Skonto 5 %] = null
|
|
||||||
,[Mahnung] = null
|
|
||||||
,[Kontoauszug] = null
|
|
||||||
,[Mahntext 1] = null
|
|
||||||
,[Mahntext 2] = null
|
|
||||||
,[Mahntext 3] = null
|
|
||||||
,[Kontoauszugstext] = null
|
|
||||||
,[Mahnlimit Betrag] = null
|
|
||||||
,[Mahnlimit %] = null
|
|
||||||
,[Zinsberechnung] = null
|
|
||||||
,[Mahnzinssatz 1] = null
|
|
||||||
,[Mahnzinssatz 2] = null
|
|
||||||
,[Mahnzinssatz 3] = null
|
|
||||||
,[Lastschrift] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Verfahren] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Mandantenbank] = null
|
|
||||||
,[Zahlungsträger] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Indiv. Feld 1] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Indiv. Feld 2] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Indiv. Feld 3] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Indiv. Feld 4] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Indiv. Feld 5] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Indiv. Feld 6] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Indiv. Feld 7] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Indiv. Feld 8] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Indiv. Feld 9] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Indiv. Feld 10] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Indiv. Feld 11] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Indiv. Feld 12] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Indiv. Feld 13] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Indiv. Feld 14] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Indiv. Feld 15] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Abweichende Anrede (Rechnungsadresse)] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Adressart (Rechnungsadresse)] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Straße (Rechnungsadresse)] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Postfach (Rechnungsadresse)] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Postleitzahl (Rechnungsadresse)] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Ort (Rechnungsadresse)] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Land (Rechnungsadresse)] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Versandzusatz (Rechnungsadresse)] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Adresszusatz (Rechnungsadresse)] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Abw. Zustellbezeichnung 1 (Rechnungsadresse)] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Abw. Zustellbezeichnung 2 (Rechnungsadresse)] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Adresse Gültig von (Rechnungsadresse)] = null
|
|
||||||
,[Adresse Gültig bis (Rechnungsadresse)] = null
|
|
||||||
,[Bankleitzahl 6] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bankbezeichnung 6] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bank-Kontonummer 6] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Länderkennzeichen 6] = [dbo].[ott_quote_255]('')
|
|
||||||
,[IBAN-Nr. 6] = [dbo].[ott_quote_255]('')
|
|
||||||
,[IBAN6 korrekt] = [dbo].[ott_quote_255]('')
|
|
||||||
,[SWIFT-Code 6] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Abw. Kontoinhaber 6] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Kennz. Hauptbankverb. 6] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bankverb 6 Gültig von] = null
|
|
||||||
,[Bankverb 6 Gültig bis] = null
|
|
||||||
,[Bankleitzahl 7] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bankbezeichnung 7] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bank-Kontonummer 7] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Länderkennzeichen 7] = [dbo].[ott_quote_255]('')
|
|
||||||
,[IBAN-Nr. 7] = [dbo].[ott_quote_255]('')
|
|
||||||
,[IBAN7 korrekt] = [dbo].[ott_quote_255]('')
|
|
||||||
,[SWIFT-Code 7] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Abw. Kontoinhaber 7] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Kennz. Hauptbankverb. 7] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bankverb 7 Gültig von] = null
|
|
||||||
,[Bankverb 7 Gültig bis] = null
|
|
||||||
,[Bankleitzahl 8] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bankbezeichnung 8] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bank-Kontonummer 8] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Länderkennzeichen 8] = [dbo].[ott_quote_255]('')
|
|
||||||
,[IBAN-Nr. 8] = [dbo].[ott_quote_255]('')
|
|
||||||
,[IBAN8 korrekt] = [dbo].[ott_quote_255]('')
|
|
||||||
,[SWIFT-Code 8] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Abw. Kontoinhaber 8] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Kennz. Hauptbankverb. 8] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bankverb 8 Gültig von] = null
|
|
||||||
,[Bankverb 8 Gültig bis] = null
|
|
||||||
,[Bankleitzahl 9] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bankbezeichnung 9] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bank-Kontonummer 9] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Länderkennzeichen 9] = [dbo].[ott_quote_255]('')
|
|
||||||
,[IBAN-Nr. 9] = [dbo].[ott_quote_255]('')
|
|
||||||
,[IBAN9 korrekt] = [dbo].[ott_quote_255]('')
|
|
||||||
,[SWIFT-Code 9] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Abw. Kontoinhaber 9] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Kennz. Hauptbankverb. 9] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bankverb 9 Gültig von] = null
|
|
||||||
,[Bankverb 9 Gültig bis] = null
|
|
||||||
,[Bankleitzahl 10] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bankbezeichnung 10] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bank-Kontonummer 10] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Länderkennzeichen 10] = [dbo].[ott_quote_255]('')
|
|
||||||
,[IBAN-Nr. 10] = [dbo].[ott_quote_255]('')
|
|
||||||
,[IBAN10 korrekt] = [dbo].[ott_quote_255]('')
|
|
||||||
,[SWIFT-Code 10] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Abw. Kontoinhaber 10] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Kennz. Hauptbankverb. 10] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Bankverb 10 Gültig von] = null
|
|
||||||
,[Bankverb 10 Gültig bis] = null
|
|
||||||
,[Nummer Fremdsystem] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Insolvent] = null
|
|
||||||
,[Mandatsreferenz 1] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Mandatsreferenz 2] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Mandatsreferenz 3] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Mandatsreferenz 4] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Mandatsreferenz 5] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Mandatsreferenz 6] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Mandatsreferenz 7] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Mandatsreferenz 8] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Mandatsreferenz 9] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Mandatsreferenz 10] = [dbo].[ott_quote_255]('')
|
|
||||||
,[Verknüpftes OPOS-Konto] = null
|
|
||||||
,[Mahnsperre bis] = null
|
|
||||||
,[Lastschriftsperre bis] = null
|
|
||||||
,[Zahlungssperre bis] = null
|
|
||||||
,[gebührenberechnung] = null
|
|
||||||
,[Mahngebühr 1] = null
|
|
||||||
,[Mahngebühr 2] = null
|
|
||||||
,[Mahngebühr 3] = null
|
|
||||||
,[Pauschalenberechnung] = null
|
|
||||||
,[Verzugspauschale 1] = null
|
|
||||||
,[Verzugspauschale 2] = null
|
|
||||||
,[Verzugspauschale 3] = null
|
|
||||||
,[Alternativer Suchname] = null
|
|
||||||
,[Status] = null
|
|
||||||
,[Anschrift manuell geändert (Korrespondenzadresse)] = null
|
|
||||||
,[Anschrift individuell (Korrespondenzadresse)] = null
|
|
||||||
,[Anschrift manuell geändert (Rechnungsadresse)] = null
|
|
||||||
,[Anschrift individuell (Rechnungsadresse)] = null
|
|
||||||
,[Fristberechnung bei Debitor] = null
|
|
||||||
,[Mahnfrist 1] = null
|
|
||||||
,[Mahnfrist 2] = null
|
|
||||||
,[Mahnfrist 3] = null
|
|
||||||
,[Letzte Frist] = null
|
|
||||||
FROM dd JOIN cc on cc.[#] = 1 AND cc.[company_id] = dd.[id]
|
|
||||||
LEFT JOIN [dbo].[mfr__#locations] as l ON l.[EntityID] = dd.[id] and l.Property = 'Company:Location'
|
|
||||||
END
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
|
|
||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__getInvRequestItems]
|
|
||||||
@invoiceid varchar(20)
|
|
||||||
, @authuser varchar(100)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
|
|
||||||
DECLARE @idbigint bigint = TRY_CAST(@invoiceid as bigint);
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @SReq TABLE ([id] bigint, [DateOfCreation] datetime, [name] nvarchar(255), [invoice.Id] varchar(20), [customer.ExternalId] bigint, [customer.Id] bigint, [mfr] bit);
|
|
||||||
|
|
||||||
INSERT INTO @SReq
|
|
||||||
SELECT s.[Id], s.[DateOfCreation], s.[name], [invoice.Id] = TRY_CAST(i.[Id] as varchar(20)), [customer.ExternalId] = cy.ExternalId , [customer.Id] = cy.[id], [mfr] = 1
|
|
||||||
FROM [dbo].[mfr__servicerequests] as s JOIN [mfr__reports] as i on i.[id] = @idbigint and s.[InvoiceId] like ('%' + i.[InvoiceId] + '%') LEFT JOIN [dbo].[mfr__companies] as cy on s.[CustomerId] = cy.id
|
|
||||||
where i.[id] = @idbigint
|
|
||||||
UNION
|
|
||||||
SELECT s.[Id], s.[DateOfCreation], s.[name], [invoice.Id] = i.[Id], [customer.ExternalId] = cy.ExternalId , [customer.Id] = cy.[id], [mfr] = 0
|
|
||||||
FROM [dbo].[mfr__servicerequests] as s
|
|
||||||
JOIN [dbo].[fds__invoice_servicerequests] as iq ON s.[id] = iq.[mfr__servicerequest]
|
|
||||||
JOIN [dbo].[fds__invoices] as i on iq.[InvId] = i.[id] AND i.[id] = @invoiceid
|
|
||||||
LEFT JOIN [dbo].[mfr__companies] as cy on s.[CustomerId] = cy.id
|
|
||||||
where i.[id] = @invoiceid
|
|
||||||
;
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
[id],[DateOfCreation],[name],[invoice.Id],[customer.ExternalId],[customer.Id], [mfr]
|
|
||||||
,[order] = ROW_NUMBER() OVER (ORDER BY [DateOfCreation])
|
|
||||||
FROM @SReq;
|
|
||||||
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
itm.[Id]
|
|
||||||
, [net_pos] = CASE WHEN LOWER(itm.[type]) in ('Title','Text') THEN '' ELSE FORMAT( CAST( (ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0) as numeric(7,2)) , '#0.00 €','de') END
|
|
||||||
, [bo_pos] = CASE WHEN LOWER(itm.[type]) in ('Title','Text') THEN '' ELSE FORMAT( CAST( ((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) * (1 + (ISNULL(vat.[vat], 19.0) * 0.01)) as numeric(7,2)) , '#0.00 €','de') END
|
|
||||||
, [vat] = CASE WHEN LOWER(itm.[type]) in ('Title','Text') THEN null ELSE FORMAT(ISNULL(vat.[vat], 19.0), '#0.0','de') + '%' END
|
|
||||||
, itm.[ServiceRequestId]
|
|
||||||
, itm.[SortOrder]
|
|
||||||
, itm.[Type]
|
|
||||||
, itm.[NameOrNumber]
|
|
||||||
, sreq.[invoice.Id]
|
|
||||||
, [order] = ROW_NUMBER() OVER (PARTITION BY itm.[ServiceRequestId] ORDER BY itm.[SortOrder])
|
|
||||||
FROM [dbo].[mfr__items] as itm
|
|
||||||
JOIN @sreq as sreq ON itm.[ServiceRequestId] = SReq.[id]
|
|
||||||
LEFT JOIN [dbo].[fds__custom_vat] as vat ON itm.[Id] = vat.[EntityId] and vat.[EntityType] = 'item'
|
|
||||||
;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
END
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__getInvoiceReminder]
|
|
||||||
@InvId varchar(15)
|
|
||||||
, @include_drafts bit = 0
|
|
||||||
,@authuser varchar(25)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
|
|
||||||
With inv as (
|
|
||||||
SELECT TOP(1)
|
|
||||||
[Id] = CAST([id] as varchar(15)), [InvoiceId], [DocumentName]
|
|
||||||
FROM [dbo].[fds__invoices]
|
|
||||||
WHERE [id] = @InvId
|
|
||||||
), inv2 as(
|
|
||||||
SELECT * FROM inv
|
|
||||||
UNION
|
|
||||||
SELECT TOP(1)
|
|
||||||
[Id] = CAST([id] as varchar(15)), [InvoiceId], [DocumentName]
|
|
||||||
FROM [dbo].[mfr__invoices] as mi
|
|
||||||
WHERE NOT EXISTS( SELECT 0 FROM inv )
|
|
||||||
AND TRY_CAST(@InvId as bigint) is not null
|
|
||||||
AND mi.[id] = TRY_CAST(@InvId as bigint)
|
|
||||||
)
|
|
||||||
SELECT rem.[Id]
|
|
||||||
,rem.[DateSent]
|
|
||||||
,[status] = CAST(CASE WHEN rem.[isSent] = 1 THEN 'versandt' WHEN rem.[isfinal] = 1 THEN 'fertig' WHEN rem.isfinal = 0 THEN 'Entwurf' ELSE '' END AS varchar(50))
|
|
||||||
,rem.[subject]
|
|
||||||
,rem.[DocumentName]
|
|
||||||
,rem.[amount_open]
|
|
||||||
,rem.[InvId]
|
|
||||||
,[InvoiceId] = inv2.[InvoiceId]
|
|
||||||
--,[InvoiceDocumentName] = inv2.[DocumentName]
|
|
||||||
,[hasFile] = cast( IIF(rem.[file] is null, 0, 1) as bit)
|
|
||||||
FROM [dbo].[fds__reminder] as rem
|
|
||||||
LEFT JOIN inv2 on rem.[InvId] = inv2.[Id]
|
|
||||||
WHERE [invId] = @InvId;
|
|
||||||
|
|
||||||
END
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__getInvoices_list2]
|
|
||||||
@tgtdate date
|
|
||||||
,@mode varchar(1) = 'm'
|
|
||||||
, @include_drafts bit = 0
|
|
||||||
, @search varchar(100)
|
|
||||||
, @authuser varchar(100)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
IF [dbo].[fis_getModuleAuth]('fds_inv', @authuser) < 1
|
|
||||||
THROW 60000, N'not authorized', 1;
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info])
|
|
||||||
VALUES ('fds__getInvoices_list' ,@authuser , @search);
|
|
||||||
|
|
||||||
DECLARE @startdate date, @enddate date, @today date = GETDATE();
|
|
||||||
DECLARE @true bit = 1, @false bit = 0;
|
|
||||||
DECLARE @searchmode as varchar(1) = CASE WHEN SUBSTRING(ISNULL(@search, '') + ' ', 2,1) <> ':' THEN '' ELSE LOWER(LEFT(@search,1 )) END;
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @customerfilter TABLE ([CustomerID] bigint);
|
|
||||||
|
|
||||||
IF @searchmode <> ''
|
|
||||||
BEGIN
|
|
||||||
SELECT @mode = 's', @search = SUBSTRING(@search,3, 1000);
|
|
||||||
|
|
||||||
IF @searchmode = 'c' AND LEN(@search) > 3
|
|
||||||
BeGIN
|
|
||||||
INSERT INTO @customerfilter SELECT [id] FROM [dbo].[mfr__companies] WHERE [Name] like ('%' + @search + '%');
|
|
||||||
END
|
|
||||||
END
|
|
||||||
ELSE
|
|
||||||
SET @mode = LOWER(@mode);
|
|
||||||
If @mode = 'm'
|
|
||||||
BEGIN
|
|
||||||
SELECT @startdate = [dbo].[date_monthfirst](@tgtdate)
|
|
||||||
,@enddate = [dbo].[date_monthend](@tgtdate);
|
|
||||||
END
|
|
||||||
ELSE If @mode = 'w'
|
|
||||||
BEGIN
|
|
||||||
SELECT @startdate = [dbo].[date_weekfirst](@tgtdate)
|
|
||||||
,@enddate = [dbo].[date_weekend](@tgtdate);
|
|
||||||
END;
|
|
||||||
|
|
||||||
|
|
||||||
--Reminder settings
|
|
||||||
DECLARE @stage1 varchar(10), @stage2 varchar(10), @stage3 varchar(10);
|
|
||||||
SELECT @stage1 = [stage1], @stage2 = [stage2], @stage3 = [stage3] FROM [dbo].[fds__admin_reminderSettings]();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
SELECT [startdate] = @startdate, [enddate] = @enddate, [mode] = @mode
|
|
||||||
, [title] = 'Rechnungsübersicht für ' + CASE WHEN @mode = 'm' THEN 'MONAT ' + FORMAT(@startdate, 'MMM yyyy')
|
|
||||||
WHEN @mode = 'w' THEN 'WOCHE ' + FORMAT(@startdate, 'dd.MM.yyyy') + ' - ' + FORMAT(@enddate, 'dd.MM.')
|
|
||||||
WHEN @searchmode = 'i' THEN 'Suche nach Rechnungsnummer ''' + @search + ''''
|
|
||||||
WHEN @searchmode = 'c' THEN 'Suche nach Kunde ''' + @search + ''''
|
|
||||||
ELSE '' END
|
|
||||||
,[ust_options] = '19,0%;16,0%;0,0%';
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @inv TABLE([Id] varchar(15), [InvoiceID] varchar(255), [URI] nvarchar(255), [DocumentName] nvarchar(255), [DateOfCreation] date, [fds] bit);
|
|
||||||
--INSERT INTO @inv SELECT [Id],[invoiceid],[URI],[DocumentName],[DateOfCreation],[fds] = 0 FROM [dbo].[mfr__reports] where [IsInvoice] = 1 AND CAST([DateOfCreation] as date) between @startdate AND @enddate;
|
|
||||||
INSERT INTO @inv SELECT [Id],[invoiceid],[URI],[DocumentName],[DateOfCreation],[fds] = 0 FROM [dbo].[mfr__invoices]
|
|
||||||
where (CASE WHEN @searchmode = 'i' THEN
|
|
||||||
(CASE WHEN [InvoiceID] = @search OR [InvoiceID] like ('R20[0-9][0-9][-]' + @search) THEN 1 ELSE 0 END)
|
|
||||||
ELSE
|
|
||||||
(CASE WHEN CAST([DateOfCreation] as date) between @startdate AND @enddate and (ISNULL(@include_drafts,0) = 1 OR [FileType] in( 'PdfInvoice','PdfCancelInvoice','PdfPartialInvoice') and ISNULL(InvoiceId,'') not in ('Preview','')) THEN 1 ELSE 0 END)
|
|
||||||
END) = 1;
|
|
||||||
INSERT INTO @inv SELECT [Id],[invoiceid],[URI] = null,[DocumentName],[DateOfCreation] = [DateCreated],[fds] = 1 FROM [dbo].[fds__invoices]
|
|
||||||
where (CASE WHEN @searchmode = 'i' THEN
|
|
||||||
(CASE WHEN [InvoiceID] = @search OR [InvoiceID] like ('R20[0-9][0-9][-]' + @search) THEN 1 ELSE 0 END)
|
|
||||||
WHEN @searchmode = 'c' THEN
|
|
||||||
(CASE WHEN [CustomerId] IN (SELECT [CustomerId] fROM @customerfilter) THEN 1 ELSE 0 END)
|
|
||||||
ELSE
|
|
||||||
(CASE WHEN CAST([DateCreated] as date) between @startdate AND @enddate and (ISNULL(@include_drafts,0) = 1 OR [isfinal] = 1) THEN 1 ELSE 0 END)
|
|
||||||
END) = 1;
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @SReq TABLE ([id] bigint, [DateOfCreation] datetime, [name] nvarchar(255), [invoice.Id] varchar(15), [customer.ExternalId] bigint, [customer.Id] bigint, [ExternalId] nvarchar(255), [fds] bit);
|
|
||||||
INSERT INTO @SReq
|
|
||||||
SELECT s.[Id], s.[DateOfCreation], s.[name], [invoice.Id] = i.[Id], [customer.ExternalId] = cy.ExternalId , [customer.Id] = cy.[id], s.[ExternalId], [fds]
|
|
||||||
FROM [dbo].[mfr__servicerequests] as s
|
|
||||||
JOIN dbo.[mfr__*PartnerSet] as p on p.[EntityId] = s.[Id] AND p.[Property] = 'ServiceRequest:Invoices'
|
|
||||||
JOIN @inv as i on i.fds = 0 and p.[PartnerId] = i.[id] --s.[InvoiceId] like ('%' + i.[InvoiceId] + '%')
|
|
||||||
LEFT JOIN [dbo].[mfr__companies] as cy on s.[CustomerId] = cy.id
|
|
||||||
WHERE i.fds = 0;
|
|
||||||
INSERT INTO @SReq
|
|
||||||
SELECT s.[Id], s.[DateOfCreation], s.[name], [invoice.Id] = i.[Id], [customer.ExternalId] = cy.ExternalId , [customer.Id] = cy.[id], s.[ExternalId], [fds]
|
|
||||||
FROM [dbo].[fds__invoice_servicerequests] as irq
|
|
||||||
JOIN @inv as i on irq.[InvId] = i.[Id]
|
|
||||||
LEFT JOIN [dbo].[mfr__servicerequests] as s ON irq.[mfr__servicerequest] = s.[id]
|
|
||||||
LEFT JOIN [dbo].[mfr__companies] as cy on s.[CustomerId] = cy.id
|
|
||||||
WHERE i.fds = 1;
|
|
||||||
|
|
||||||
With itm as (
|
|
||||||
SELECT
|
|
||||||
[net_itemsum] = CAST( SUM((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) as numeric(12,2))
|
|
||||||
, [bo_itemsum] = CAST( SUM( ((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) * (1 + (ISNULL(vat.[vat], 19.0) * 0.01))) as numeric(12,2))
|
|
||||||
, [vf] = MAX(1 + (ISNULL(vat.[vat], 19.0) * 0.01))
|
|
||||||
, sreq.[invoice.Id]
|
|
||||||
,[fds] = 0
|
|
||||||
FROM [dbo].[mfr__items] as itm
|
|
||||||
JOIN @sreq as sreq ON itm.[ServiceRequestId] = SReq.[id]
|
|
||||||
LEFT JOIN [dbo].[fds__custom_vat] as vat ON itm.[Id] = vat.[EntityId] and vat.[EntityType] = 'item'
|
|
||||||
WHERE [fds] = 0
|
|
||||||
GROUP BY sreq.[invoice.Id]
|
|
||||||
UNION
|
|
||||||
SELECT
|
|
||||||
[net_itemsum] = CAST( SUM(ISNULL([value_total],0)) as numeric(12,2))
|
|
||||||
, [bo_itemsum] = CAST( SUM(ISNULL([value_total],0) * (1 + (ISNULL([vat], 19.0) * 0.01))) as numeric(12,2))
|
|
||||||
, [vf] = MAX(1 + (ISNULL([vat], 19.0) * 0.01))
|
|
||||||
, [invoice.Id] = inv.[id] --sreq.[invoice.Id]
|
|
||||||
, [fds] = 1
|
|
||||||
FROM [dbo].[fds__invoice_items] as itm
|
|
||||||
--JOIN @sreq as sreq ON itm.[InvRqId] = SReq.[id]
|
|
||||||
JOIN @inv as inv ON itm.[invid] = inv.[id] and [fds] = 1
|
|
||||||
WHERE [fds] = 1
|
|
||||||
GROUP BY inv.[id] --sreq.[invoice.Id]
|
|
||||||
),sc as(
|
|
||||||
SELECT s.[invoice.Id], [fds], [#] = COUNT(s.[id]) FROM @SReq as s GROUP BY s.[invoice.Id], [fds]
|
|
||||||
),contra_accounts as(
|
|
||||||
SELECT [account] = [key], [vat] = TRY_PARSE([value2] as numeric(5,2) USING 'en-US') FROM [dbo].[fds__admin_settings] WHERE [type] = 'contra_account'
|
|
||||||
), inv as(
|
|
||||||
--select
|
|
||||||
-- [Id] = CAST(iv.[Id] as varchar(15))
|
|
||||||
-- ,[InvoiceId] = iv.[InvoiceId]
|
|
||||||
-- ,[InvoiceType] = CAST( CASE WHEN LEFT([documentname],5) = 'absch' THEN 'i'
|
|
||||||
-- WHEN LEFT([documentname],5) = 'storn' THEN 'c'
|
|
||||||
-- ELSE 'r'
|
|
||||||
-- END
|
|
||||||
-- as char(1))
|
|
||||||
-- ,[DocumentName]
|
|
||||||
-- ,[requestcount] = sc.[#]
|
|
||||||
-- ,[request] = (SELECT TOP(1) s.[ExternalId] FROM @SReq as s WHERE s.[invoice.Id] = iv.id)
|
|
||||||
-- ,[balance] = FORMAT(ABS([invoiceBalance]), '#0.00', 'de') + ' €'
|
|
||||||
-- ,[EINZELPOS_brutto] = FORMAT(itm.bo_itemsum, '#0.00', 'de') + ' €'
|
|
||||||
-- ,[EINZELPOS_netto] = FORMAT(itm.net_itemsum, '#0.00', 'de') + ' €'
|
|
||||||
-- ,[vat] = FORMAT(ISNULL(vat.[vat], 19.0), '#0.0', 'de') + ' %'
|
|
||||||
-- ,[deb_cred] = CASE WHEN [invoiceBalance] >= 0 THEN 'S' ELSE 'H' END
|
|
||||||
-- ,[customer] = (SELECT TOP(1) [customer.ExternalId] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id] and [customer.ExternalId] is not null ORDER BY SReq.DateOfCreation ASC)
|
|
||||||
-- ,[contra_account] = ca.[account]
|
|
||||||
-- ,[Belegdatum] = FORMAT(iv.[DateOfCreation], 'dd.MM.yy')
|
|
||||||
-- ,[Buchungstext] = (SELECT TOP(1) SReq.[name] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id])
|
|
||||||
-- ,iv.[DateOfCreation]
|
|
||||||
-- ,iv.[DueDate]
|
|
||||||
-- ,[PaymentTerm] = @stage1
|
|
||||||
-- ,ivi.[PaymentStatus]
|
|
||||||
-- ,[IsPayed] = CAST(CASE WHEN ISNULL(iv.[IsCanceled],0) = 1 THEN 1 ELSE ISNULL(ivi.[IsPayed],0) END as bit)
|
|
||||||
-- ,[IsCanceled]
|
|
||||||
-- ,[isSent] = @true
|
|
||||||
-- ,[isFinal] = @true
|
|
||||||
-- ,[hasFile] = CAST( CASE wHEN ISNULL([URI], '') <> '' THEN 1 ELSE 0 END as bit)
|
|
||||||
-- ,[fds] = @false
|
|
||||||
-- from [dbo].[mfr__reports] as iv
|
|
||||||
-- LEFT JOIN [dbo].[fds__custom_invoiceinfo] as ivi on iv.[id] = ivi.[invid]
|
|
||||||
-- LEFT JOIN itm on iv.Id = itm.[invoice.Id]
|
|
||||||
-- LEFT JOIN [dbo].[fds__custom_vat] as vat ON iv.[Id] = vat.[EntityId] and vat.[EntityType] = 'report'
|
|
||||||
-- LEFT JOIN sc on iv.[id] = sc.[invoice.Id] and sc.[fds] = 0
|
|
||||||
-- LEFT JOIN contra_accounts as ca ON ISNULL(vat.[vat], 19.0) = ca.[vat]
|
|
||||||
-- where iv.Id in (SELECT [Id] FROM @inv Where fds = 0)
|
|
||||||
--UNION
|
|
||||||
select
|
|
||||||
[Id] = CAST(iv.[Id] as varchar(15))
|
|
||||||
,[InvoiceId] = iv.[InvoiceId]
|
|
||||||
,[InvoiceType] = CAST( CASE WHEN LEFT([documentname],5) = 'absch' THEN 'i'
|
|
||||||
WHEN LEFT([documentname],5) = 'storn' THEN 'c'
|
|
||||||
ELSE 'r'
|
|
||||||
END
|
|
||||||
as char(1))
|
|
||||||
,[DocumentName]
|
|
||||||
,[requestcount] = sc.[#]
|
|
||||||
,[request] = (SELECT TOP(1) s.[ExternalId] FROM @SReq as s WHERE s.[invoice.Id] = TRY_CAST(iv.id as varchar(50)))
|
|
||||||
,[balance] = FORMAT([invoiceBalance], '#0.00', 'de') + ' €'
|
|
||||||
,[EINZELPOS_brutto] = NULL --FORMAT(itm.bo_itemsum, '#0.00', 'de') + ' €'
|
|
||||||
,[EINZELPOS_netto] = NULL --FORMAT(itm.net_itemsum, '#0.00', 'de') + ' €'
|
|
||||||
,[vat] = FORMAT(ISNULL(vat.[vat], ISNULL([dbo].[fds__fn_vatfrombalances]([invoiceBalance], [invoiceBalancenetto]) ,19.0)), '#0.0', 'de') + ' %'
|
|
||||||
,[deb_cred] = CASE WHEN [invoiceBalance] >= 0 THEN 'S' ELSE 'H' END
|
|
||||||
--,[customer] = (SELECT TOP(1) [customer.ExternalId] FROM @SReq as sreq WHERE TRY_CAST(iv.id as varchar(50)) = sreq.[invoice.Id] and [customer.ExternalId] is not null ORDER BY SReq.DateOfCreation ASC)
|
|
||||||
,[contra_account] = ca.[account]
|
|
||||||
,[Belegdatum] = FORMAT(iv.[DateOfCreation], 'dd.MM.yy')
|
|
||||||
,[Buchungstext] = (SELECT TOP(1) SReq.[name] FROM @SReq as sreq WHERE TRY_CAST(iv.id as varchar(50)) = sreq.[invoice.Id])
|
|
||||||
,iv.[DateOfCreation]
|
|
||||||
,iv.[DueDate]
|
|
||||||
,[PaymentTerm] = @stage1
|
|
||||||
,ivi.[PaymentStatus]
|
|
||||||
,[IsPayed] = CAST(CASE WHEN ISNULL(ivi.isPayed,0) = 1 THEN 1 WHEN [InvoiceState] = 'eIsCancelled' THEN 1 WHEN [InvoiceState] = 'eIsPaid' THEN 1 ELSE 0 END as bit)
|
|
||||||
,[IsCanceled] = CAST(CASE WHEN [InvoiceState] = 'eIsCancelled' THEN 1 ELSE 0 END as bit)
|
|
||||||
,[isSent] = @true
|
|
||||||
,[isFinal] = @true
|
|
||||||
,[hasFile] = CAST( CASE wHEN ISNULL([URI], '') <> '' THEN 1 ELSE 0 END as bit)
|
|
||||||
,[fds] = @false
|
|
||||||
,[customer_id] = ps.[PartnerId]
|
|
||||||
from [dbo].[mfr__invoices] as iv
|
|
||||||
LEFT JOIN [dbo].[fds__custom_invoiceinfo] as ivi on iv.[id] = ivi.[invid]
|
|
||||||
LEFT JOIN itm on TRY_CAST(iv.id as varchar(50)) = itm.[invoice.Id]
|
|
||||||
LEFT JOIN [dbo].[fds__custom_vat] as vat ON iv.[Id] = vat.[EntityId] and vat.[EntityType] = 'report'
|
|
||||||
LEFT JOIN sc on iv.[id] = sc.[invoice.Id] and sc.[fds] = 0
|
|
||||||
LEFT JOIN contra_accounts as ca ON ISNULL(vat.[vat], 19.0) = ca.[vat]
|
|
||||||
LEFT JOIN [dbo].[mfr__*PartnerSet] as ps ON iv.[Id] = ps.[EntityId] AND ps.[Property] = 'Invoice:Customer'
|
|
||||||
where TRY_CAST(iv.id as varchar(50)) in (SELECT [Id] FROM @inv Where fds = 0)
|
|
||||||
UNION
|
|
||||||
select
|
|
||||||
iv.[Id]
|
|
||||||
,[InvoiceId] = iv.[InvoiceId] + ISNULL((CASE WHEN iv.InvoiceType = 'c' THEN (SELECT TOP(1) ' >> ' + i2.[InvoiceId] FROM [dbo].[fds__invoices] as i2 JOIN [dbo].[fds__invoice_details] as id2 on i2.[Id] = id2.[StornoTo_InvId] where id2.[InvId] = iv.[id] and i2.[IsFinal] = 1) ELSE '' END), '')
|
|
||||||
,[InvoiceType]
|
|
||||||
,[DocumentName]
|
|
||||||
,[requestcount] = sc.[#]
|
|
||||||
,[request] = (SELECT TOP(1) s.[ExternalId] FROM @SReq as s WHERE s.[invoice.Id] = iv.id)
|
|
||||||
,[balance] = FORMAT([invoiceBalance], '#0.00', 'de') + ' €'
|
|
||||||
,[EINZELPOS_brutto] = NULL --FORMAT(itm.bo_itemsum, '#0.00', 'de') + ' €'
|
|
||||||
,[EINZELPOS_netto] = NULL --FORMAT(itm.net_itemsum, '#0.00', 'de') + ' €'
|
|
||||||
,[vat] = FORMAT(ISNULL(iv.[InvoiceVAT_1], 19.0), '#0.0', 'de') + ' %'
|
|
||||||
,[deb_cred] = CASE WHEN [invoiceBalance] >= 0 THEN 'S' ELSE 'H' END
|
|
||||||
--,[customer] = (SELECT TOP(1) [customer.ExternalId] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id] and [customer.ExternalId] is not null ORDER BY SReq.DateOfCreation ASC)
|
|
||||||
,[contra_account] = ca.[account]
|
|
||||||
,[Belegdatum] = FORMAT(iv.[DateCreated], 'dd.MM.yy')
|
|
||||||
,[Buchungstext] = (SELECT TOP(1) SReq.[name] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id])
|
|
||||||
,[DateOfCreation] = iv.[DateCreated]
|
|
||||||
,[DueDate]
|
|
||||||
,[PaymentTerm] = ISNULL([PaymentTerm], @stage1)
|
|
||||||
,[PaymentStatus]
|
|
||||||
,[IsPayed] = CAST(CASE WHEN ISNULL(iv.[IsCanceled],0) = 1 THEN 1 ELSE ISNULL([IsPayed],0) END as bit)
|
|
||||||
,[IsCanceled]
|
|
||||||
,[isSent]
|
|
||||||
,[isFinal]
|
|
||||||
,[hasFile] = CAST( CASE wHEN [file] is not null and [DocumentNAme] is not null THEN 1 ELSE 0 END as bit)
|
|
||||||
,[fds] = @true
|
|
||||||
,[customer_id] = iv.[CustomerId]
|
|
||||||
from [dbo].[fds__invoices] as iv
|
|
||||||
LEFT JOIN itm on iv.Id = itm.[invoice.Id]
|
|
||||||
LEFT JOIN sc on iv.[id] = sc.[invoice.Id] and sc.[fds] = 1
|
|
||||||
LEFT JOIN contra_accounts as ca ON ISNULL(iv.[InvoiceVAT_1], 19.0) = ca.[vat]
|
|
||||||
where iv.Id in (SELECT [Id] FROM @inv Where fds = 1)
|
|
||||||
), reminder as (
|
|
||||||
SELECT [InvId], [lastReminder] = MAX([DateFinalized]), [#] = COUNT(*) FROM [dbo].[fds__reminder] WHERE [IsFinal] = 1 AND [invId] IN (SELECT [Id] FROM @inv) GROUP BY [InvId]
|
|
||||||
)
|
|
||||||
select
|
|
||||||
inv.*
|
|
||||||
,[reminderstatus] = CASE WHEN [ispayed] = 1 THEN 0
|
|
||||||
WHEN [dbo].[fds__fn_isInvoiceCancelled](inv.[Id]) = 1 THEN 0
|
|
||||||
WHEN DueDate is null THEN 0
|
|
||||||
WHEN [deb_cred] = 'H' THEN 0
|
|
||||||
WHEN @stage3 <> '' AND rem.[#] = 2 AND @today > [dbo].[date_addcustom]([lastReminder], @stage3) THEN 3
|
|
||||||
WHEN @stage2 <> '' AND rem.[#] = 1 AND @today > [dbo].[date_addcustom]([lastReminder], @stage2) THEN 2
|
|
||||||
WHEN @stage1 <> '' AND ISNULL(rem.[#],0) = 0 AND @today > [dbo].[date_addcustom](ISNULL(DueDate, [DateOfCreation]), ISNULL([PaymentTerm],@stage1)) THEN 1
|
|
||||||
ELSE 0 END
|
|
||||||
,[invstatus] = CASE WHEN [IsPayed] = 1 AND [isCanceled] = 0 THEN 'pyd'
|
|
||||||
WHEN [isCanceled] = 1 OR [dbo].[fds__fn_isInvoiceCancelled](inv.[Id]) = 1 THEN 'cc'
|
|
||||||
WHEN [isFinal] = 0 THEN 'dft'
|
|
||||||
WHEN [isFinal] = 1 AND [isSent] = 0 THEN 'uns'
|
|
||||||
WHEN [DueDate] is null THEN 'op'
|
|
||||||
WHEN [deb_cred] = 'H' THEN ''
|
|
||||||
WHEN @today >= [DueDate] THEN 'due'
|
|
||||||
WHEN @stage1 <> '' AND ISNULL(rem.[#],0) = 0 AND @today > [dbo].[date_addcustom](ISNULL(DueDate, [DateOfCreation]), @stage1) THEN 'ovd'
|
|
||||||
WHEN ISNULL(rem.[#],0) > 0 THEN 'rem'
|
|
||||||
END
|
|
||||||
,[reminder] = rem.[#]
|
|
||||||
,[CustomerName] = c.[name]
|
|
||||||
,[order] = CASE WHEN @include_drafts = 1 THEN
|
|
||||||
ROW_NUMBER() OVER (ORDER BY inv.[DateOfCreation] ASC)
|
|
||||||
ELSE
|
|
||||||
ROW_NUMBER() OVER (ORDER BY TRY_CAST(SUBSTRING([invoiceid], 2,4) AS int), TRY_CAST(SUBSTRING([invoiceid], 7,4) AS int), inv.[DateOfCreation] ASC)
|
|
||||||
END
|
|
||||||
,[Payment] = CASE WHEN [paymentstatus] = 'a' THEN 'vollst.'
|
|
||||||
WHEN [paymentstatus] = 'p' THEN FORMAT(ISNULL([dbo].[fds__fn_InvoicePaymentAmount](inv.[Id]),0.0), '0.00€', 'de')
|
|
||||||
ELSE '' END
|
|
||||||
FROM inv
|
|
||||||
left JOIN reminder as rem ON inv.[Id] = rem.[InvId] --inv.[id] is already converted to varchar
|
|
||||||
LEFT JOIN [dbo].[mfr__companies] as c ON inv.[customer_id] = c.[id]
|
|
||||||
WHERE (CASE WHEN @searchmode = 'c' THEN
|
|
||||||
(CASE WHEN inv.[customer_id] IN (SELECT [CustomerId] fROM @customerfilter) THEN 1 ELSE 0 END)
|
|
||||||
ELSE 1 END) = 1
|
|
||||||
order by inv.[DateOfCreation] ASC;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
END
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__getInvoices_list_vario]
|
|
||||||
@tgtdate date
|
|
||||||
,@mode varchar(1) = 'm'
|
|
||||||
, @includes varchar(50)
|
|
||||||
, @authuser varchar(100)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
|
|
||||||
DECLARE @include_drafts bit = [dbo].[ott_csv_contains](@includes, 'd');
|
|
||||||
DECLARE @only_drafts bit = 0, @only_unpayed bit = 0, @only_reminded bit = 0, @only_akonto bit = 0, @only_storno bit = 0, @only_remindedunpayed bit = 0;
|
|
||||||
DECLARE @includetext varchar(50) = '';
|
|
||||||
|
|
||||||
IF @includes = '#d'
|
|
||||||
SELECT @includetext = ' (nur Entwürfe) ', @include_drafts = 1, @only_drafts = 1, @only_reminded = 0, @only_unpayed = 0, @only_akonto = 0, @only_storno = 0;
|
|
||||||
ELSE IF @includes = 'all'
|
|
||||||
SELECT @includetext = ' (alle) ', @include_drafts = 1, @only_drafts = 0, @only_reminded = 0, @only_unpayed = 0, @only_akonto = 0, @only_storno = 0;
|
|
||||||
ELSE IF @includes = '#r'
|
|
||||||
SELECT @includetext = ' (nur angemahnte) ', @include_drafts = 0, @only_drafts = 0, @only_reminded = 1, @only_unpayed = 0, @only_akonto = 0, @only_storno = 0;
|
|
||||||
ELSE IF @includes = '#u'
|
|
||||||
SELECT @includetext = ' (nur unbezahlte) ', @include_drafts = 0, @only_drafts = 0, @only_reminded = 0, @only_unpayed = 1, @only_akonto = 0, @only_storno = 0;
|
|
||||||
ELSE IF @includes = '#a'
|
|
||||||
SELECT @includetext = ' (nur Akonto) ', @include_drafts = 0, @only_drafts = 0, @only_reminded = 0, @only_unpayed = 0, @only_akonto = 1, @only_storno = 0;
|
|
||||||
ELSE IF @includes = '#c'
|
|
||||||
SELECT @includetext = ' (nur Storno) ', @include_drafts = 0, @only_drafts = 0, @only_reminded = 0, @only_unpayed = 0, @only_akonto = 0, @only_storno = 1;
|
|
||||||
ELSE IF @includes = '#ru'
|
|
||||||
SELECT @includetext = ' (nur gemahnt + unbez. + nicht storniert) ', @include_drafts = 0, @only_drafts = 0, @only_reminded = 0, @only_unpayed = 0, @only_akonto = 0, @only_storno = 0, @only_remindedunpayed = 1;
|
|
||||||
|
|
||||||
|
|
||||||
IF [dbo].[fis_getModuleAuth]('fds_inv', @authuser) < 1
|
|
||||||
THROW 60000, N'not authorized', 1;
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info])
|
|
||||||
VALUES ('fds__getInvoices_list' ,@authuser , '');
|
|
||||||
|
|
||||||
DECLARE @startdate date, @enddate date, @today date = GETDATE();
|
|
||||||
DECLARE @true bit = 1, @false bit = 0;
|
|
||||||
SET @mode = LOWER(@mode);
|
|
||||||
If @mode = 'm'
|
|
||||||
BEGIN
|
|
||||||
SELECT @startdate = [dbo].[date_monthfirst](@tgtdate)
|
|
||||||
,@enddate = [dbo].[date_monthend](@tgtdate);
|
|
||||||
END
|
|
||||||
ELSE If @mode = 'w'
|
|
||||||
BEGIN
|
|
||||||
SELECT @startdate = [dbo].[date_weekfirst](@tgtdate)
|
|
||||||
,@enddate = [dbo].[date_weekend](@tgtdate);
|
|
||||||
END
|
|
||||||
ELSE If @mode = 'a'
|
|
||||||
BEGIN
|
|
||||||
SELECT @startdate = [dbo].[date_weekfirst]('2000-01-01')
|
|
||||||
,@enddate = [dbo].[date_weekend](DATEADD(year, 1, @today));
|
|
||||||
END
|
|
||||||
|
|
||||||
--Reminder settings
|
|
||||||
DECLARE @stage1 varchar(10), @stage2 varchar(10), @stage3 varchar(10);
|
|
||||||
SELECT @stage1 = [stage1], @stage2 = [stage2], @stage3 = [stage3] FROM [dbo].[fds__admin_reminderSettings]();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
SELECT [startdate] = @startdate, [enddate] = @enddate, [mode] = @mode
|
|
||||||
, [title] = 'Rechnungsübersicht ' + @includetext + ' für ' + CASE WHEN @mode = 'm' THEN 'MONAT ' + FORMAT(@startdate, 'MMM yyyy')
|
|
||||||
WHEN @mode = 'w' THEN 'WOCHE ' + FORMAT(@startdate, 'dd.MM.yyyy') + ' - ' + FORMAT(@enddate, 'dd.MM.')
|
|
||||||
WHEN @mode = 'a' THEN 'den gesamten Zeitraum'
|
|
||||||
ELSE '' END
|
|
||||||
,[ust_options] = '19,0%;16,0%;0,0%';
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @inv TABLE([Id] varchar(15), [InvoiceID] varchar(255), [URI] nvarchar(255), [DocumentName] nvarchar(255), [DateOfCreation] date, [fds] bit);
|
|
||||||
--INSERT INTO @inv SELECT [Id],[invoiceid],[URI],[DocumentName],[DateOfCreation],[fds] = 0 FROM [dbo].[mfr__reports] where [IsInvoice] = 1 AND CAST([DateOfCreation] as date) between @startdate AND @enddate;
|
|
||||||
INSERT INTO @inv SELECT [Id],[invoiceid],[URI],[DocumentName],[DateOfCreation],[fds] = 0 FROM [dbo].[mfr__invoices]
|
|
||||||
where CAST([DateOfCreation] as date) between @startdate AND @enddate
|
|
||||||
and (ISNULL(@include_drafts,0) = 1 OR [FileType] in( 'PdfInvoice','PdfCancelInvoice','PdfPartialInvoice') and ISNULL(InvoiceId,'') not in ('Preview',''))
|
|
||||||
and (@only_drafts = 0 OR ISNULL(InvoiceId,'') in ('Preview',''))
|
|
||||||
and (@only_akonto = 0 OR [FileType] = 'PdfPartialInvoice')
|
|
||||||
and (@only_storno = 0 OR [FileType] = 'PdfCancelInvoice')
|
|
||||||
and ((@only_unpayed = 0 AND @only_remindedunpayed = 0) OR [InvoiceState] not IN ('eIsCancelled', 'eIsPaid'));
|
|
||||||
INSERT INTO @inv SELECT [Id],[invoiceid],[URI] = null,[DocumentName],[DateOfCreation] = [DateCreated],[fds] = 1 FROM [dbo].[fds__invoices]
|
|
||||||
where CAST([DateCreated] as date) between @startdate AND @enddate
|
|
||||||
and (ISNULL(@include_drafts,0) = 1 OR [isfinal] = 1)
|
|
||||||
and (@only_drafts = 0 OR [isfinal] = 0)
|
|
||||||
and (@only_akonto = 0 OR [InvoiceType] = 'i')
|
|
||||||
and (@only_storno = 0 OR [InvoiceType] = 'c')
|
|
||||||
and ((@only_unpayed = 0 AND @only_remindedunpayed = 0) OR (IsPayed = 0 AND IsCanceled = 0));
|
|
||||||
|
|
||||||
DECLARE @SReq TABLE ([id] bigint, [DateOfCreation] datetime, [name] nvarchar(255), [invoice.Id] varchar(15), [customer.ExternalId] bigint, [customer.Id] bigint, [ExternalId] nvarchar(255), [fds] bit);
|
|
||||||
INSERT INTO @SReq
|
|
||||||
SELECT s.[Id], s.[DateOfCreation], s.[name], [invoice.Id] = i.[Id], [customer.ExternalId] = cy.ExternalId , [customer.Id] = cy.[id], s.[ExternalId], [fds]
|
|
||||||
FROM [dbo].[mfr__servicerequests] as s
|
|
||||||
JOIN dbo.[mfr__*PartnerSet] as p on p.[EntityId] = s.[Id] AND p.[Property] = 'ServiceRequest:Invoices'
|
|
||||||
JOIN @inv as i on i.fds = 0 and p.[PartnerId] = i.[id] --s.[InvoiceId] like ('%' + i.[InvoiceId] + '%')
|
|
||||||
LEFT JOIN [dbo].[mfr__companies] as cy on s.[CustomerId] = cy.id
|
|
||||||
WHERE i.fds = 0;
|
|
||||||
INSERT INTO @SReq
|
|
||||||
SELECT s.[Id], s.[DateOfCreation], s.[name], [invoice.Id] = i.[Id], [customer.ExternalId] = cy.ExternalId , [customer.Id] = cy.[id], s.[ExternalId], [fds]
|
|
||||||
FROM [dbo].[fds__invoice_servicerequests] as irq
|
|
||||||
JOIN @inv as i on irq.[InvId] = i.[Id]
|
|
||||||
LEFT JOIN [dbo].[mfr__servicerequests] as s ON irq.[mfr__servicerequest] = s.[id]
|
|
||||||
LEFT JOIN [dbo].[mfr__companies] as cy on s.[CustomerId] = cy.id
|
|
||||||
WHERE i.fds = 1;
|
|
||||||
|
|
||||||
With itm as (
|
|
||||||
SELECT
|
|
||||||
[net_itemsum] = CAST( SUM((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) as numeric(12,2))
|
|
||||||
, [bo_itemsum] = CAST( SUM( ((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) * (1 + (ISNULL(vat.[vat], 19.0) * 0.01))) as numeric(12,2))
|
|
||||||
, [vf] = MAX(1 + (ISNULL(vat.[vat], 19.0) * 0.01))
|
|
||||||
, sreq.[invoice.Id]
|
|
||||||
,[fds] = 0
|
|
||||||
FROM [dbo].[mfr__items] as itm
|
|
||||||
JOIN @sreq as sreq ON itm.[ServiceRequestId] = SReq.[id]
|
|
||||||
LEFT JOIN [dbo].[fds__custom_vat] as vat ON itm.[Id] = vat.[EntityId] and vat.[EntityType] = 'item'
|
|
||||||
WHERE [fds] = 0
|
|
||||||
GROUP BY sreq.[invoice.Id]
|
|
||||||
UNION
|
|
||||||
SELECT
|
|
||||||
[net_itemsum] = CAST( SUM(ISNULL([value_total],0)) as numeric(12,2))
|
|
||||||
, [bo_itemsum] = CAST( SUM(ISNULL([value_total],0) * (1 + (ISNULL([vat], 19.0) * 0.01))) as numeric(12,2))
|
|
||||||
, [vf] = MAX(1 + (ISNULL([vat], 19.0) * 0.01))
|
|
||||||
, [invoice.Id] = inv.[id] --sreq.[invoice.Id]
|
|
||||||
, [fds] = 1
|
|
||||||
FROM [dbo].[fds__invoice_items] as itm
|
|
||||||
--JOIN @sreq as sreq ON itm.[InvRqId] = SReq.[id]
|
|
||||||
JOIN @inv as inv ON itm.[invid] = inv.[id] and [fds] = 1
|
|
||||||
WHERE [fds] = 1
|
|
||||||
GROUP BY inv.[id] --sreq.[invoice.Id]
|
|
||||||
),sc as(
|
|
||||||
SELECT s.[invoice.Id], [fds], [#] = COUNT(s.[id]) FROM @SReq as s GROUP BY s.[invoice.Id], [fds]
|
|
||||||
),contra_accounts as(
|
|
||||||
SELECT [account] = [key], [vat] = TRY_PARSE([value2] as numeric(5,2) USING 'en-US') FROM [dbo].[fds__admin_settings] WHERE [type] = 'contra_account'
|
|
||||||
), inv as(
|
|
||||||
--select
|
|
||||||
-- [Id] = CAST(iv.[Id] as varchar(15))
|
|
||||||
-- ,[InvoiceId] = iv.[InvoiceId]
|
|
||||||
-- ,[InvoiceType] = CAST( CASE WHEN LEFT([documentname],5) = 'absch' THEN 'i'
|
|
||||||
-- WHEN LEFT([documentname],5) = 'storn' THEN 'c'
|
|
||||||
-- ELSE 'r'
|
|
||||||
-- END
|
|
||||||
-- as char(1))
|
|
||||||
-- ,[DocumentName]
|
|
||||||
-- ,[requestcount] = sc.[#]
|
|
||||||
-- ,[request] = (SELECT TOP(1) s.[ExternalId] FROM @SReq as s WHERE s.[invoice.Id] = iv.id)
|
|
||||||
-- ,[balance] = FORMAT(ABS([invoiceBalance]), '#0.00', 'de') + ' €'
|
|
||||||
-- ,[EINZELPOS_brutto] = FORMAT(itm.bo_itemsum, '#0.00', 'de') + ' €'
|
|
||||||
-- ,[EINZELPOS_netto] = FORMAT(itm.net_itemsum, '#0.00', 'de') + ' €'
|
|
||||||
-- ,[vat] = FORMAT(ISNULL(vat.[vat], 19.0), '#0.0', 'de') + ' %'
|
|
||||||
-- ,[deb_cred] = CASE WHEN [invoiceBalance] >= 0 THEN 'S' ELSE 'H' END
|
|
||||||
-- ,[customer] = (SELECT TOP(1) [customer.ExternalId] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id] and [customer.ExternalId] is not null ORDER BY SReq.DateOfCreation ASC)
|
|
||||||
-- ,[contra_account] = ca.[account]
|
|
||||||
-- ,[Belegdatum] = FORMAT(iv.[DateOfCreation], 'dd.MM.yy')
|
|
||||||
-- ,[Buchungstext] = (SELECT TOP(1) SReq.[name] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id])
|
|
||||||
-- ,iv.[DateOfCreation]
|
|
||||||
-- ,iv.[DueDate]
|
|
||||||
-- ,[PaymentTerm] = @stage1
|
|
||||||
-- ,ivi.[PaymentStatus]
|
|
||||||
-- ,[IsPayed] = CAST(CASE WHEN ISNULL(iv.[IsCanceled],0) = 1 THEN 1 ELSE ISNULL(ivi.[IsPayed],0) END as bit)
|
|
||||||
-- ,[IsCanceled]
|
|
||||||
-- ,[isSent] = @true
|
|
||||||
-- ,[isFinal] = @true
|
|
||||||
-- ,[hasFile] = CAST( CASE wHEN ISNULL([URI], '') <> '' THEN 1 ELSE 0 END as bit)
|
|
||||||
-- ,[fds] = @false
|
|
||||||
-- from [dbo].[mfr__reports] as iv
|
|
||||||
-- LEFT JOIN [dbo].[fds__custom_invoiceinfo] as ivi on iv.[id] = ivi.[invid]
|
|
||||||
-- LEFT JOIN itm on iv.Id = itm.[invoice.Id]
|
|
||||||
-- LEFT JOIN [dbo].[fds__custom_vat] as vat ON iv.[Id] = vat.[EntityId] and vat.[EntityType] = 'report'
|
|
||||||
-- LEFT JOIN sc on iv.[id] = sc.[invoice.Id] and sc.[fds] = 0
|
|
||||||
-- LEFT JOIN contra_accounts as ca ON ISNULL(vat.[vat], 19.0) = ca.[vat]
|
|
||||||
-- where iv.Id in (SELECT [Id] FROM @inv Where fds = 0)
|
|
||||||
--UNION
|
|
||||||
select
|
|
||||||
[Id] = CAST(iv.[Id] as varchar(15))
|
|
||||||
,[InvoiceId] = iv.[InvoiceId]
|
|
||||||
,[InvoiceType] = CAST( CASE WHEN LEFT([documentname],5) = 'absch' THEN 'i'
|
|
||||||
WHEN LEFT([documentname],5) = 'storn' THEN 'c'
|
|
||||||
ELSE 'r'
|
|
||||||
END
|
|
||||||
as char(1))
|
|
||||||
,[DocumentName]
|
|
||||||
,[requestcount] = sc.[#]
|
|
||||||
,[request] = (SELECT TOP(1) s.[ExternalId] FROM @SReq as s WHERE s.[invoice.Id] = TRY_CAST(iv.id as varchar(50)))
|
|
||||||
,[balance] = FORMAT([invoiceBalance], '#0.00', 'de') + ' €'
|
|
||||||
,[EINZELPOS_brutto] = NULL --FORMAT(itm.bo_itemsum, '#0.00', 'de') + ' €'
|
|
||||||
,[EINZELPOS_netto] = NULL --FORMAT(itm.net_itemsum, '#0.00', 'de') + ' €'
|
|
||||||
,[vat] = FORMAT(ISNULL(vat.[vat], ISNULL([dbo].[fds__fn_vatfrombalances]([invoiceBalance], [invoiceBalancenetto]) ,19.0)), '#0.0', 'de') + ' %'
|
|
||||||
,[deb_cred] = CASE WHEN [invoiceBalance] >= 0 THEN 'S' ELSE 'H' END
|
|
||||||
,[customer] = (SELECT TOP(1) [customer.ExternalId] FROM @SReq as sreq WHERE TRY_CAST(iv.id as varchar(50)) = sreq.[invoice.Id] and [customer.ExternalId] is not null ORDER BY SReq.DateOfCreation ASC)
|
|
||||||
,[contra_account] = ca.[account]
|
|
||||||
,[Belegdatum] = FORMAT(iv.[DateOfCreation], 'dd.MM.yy')
|
|
||||||
,[Buchungstext] = (SELECT TOP(1) SReq.[name] FROM @SReq as sreq WHERE TRY_CAST(iv.id as varchar(50)) = sreq.[invoice.Id])
|
|
||||||
,iv.[DateOfCreation]
|
|
||||||
,iv.[DueDate]
|
|
||||||
,[PaymentTerm] = @stage1
|
|
||||||
,ivi.[PaymentStatus]
|
|
||||||
,[IsPayed] = CAST(CASE WHEN ISNULL(ivi.isPayed,0) = 1 THEN 1 WHEN [InvoiceState] = 'eIsCancelled' THEN 1 WHEN [InvoiceState] = 'eIsPaid' THEN 1 ELSE 0 END as bit)
|
|
||||||
,[IsCanceled] = CAST(CASE WHEN [InvoiceState] = 'eIsCancelled' THEN 1 ELSE 0 END as bit)
|
|
||||||
,[isSent] = @true
|
|
||||||
,[isFinal] = @true
|
|
||||||
,[hasFile] = CAST( CASE wHEN ISNULL([URI], '') <> '' THEN 1 ELSE 0 END as bit)
|
|
||||||
,[fds] = @false
|
|
||||||
,[customer_id] = ps.[PartnerId]
|
|
||||||
from [dbo].[mfr__invoices] as iv
|
|
||||||
LEFT JOIN [dbo].[fds__custom_invoiceinfo] as ivi on iv.[id] = ivi.[invid]
|
|
||||||
LEFT JOIN itm on TRY_CAST(iv.id as varchar(50)) = itm.[invoice.Id]
|
|
||||||
LEFT JOIN [dbo].[fds__custom_vat] as vat ON iv.[Id] = vat.[EntityId] and vat.[EntityType] = 'report'
|
|
||||||
LEFT JOIN sc on iv.[id] = sc.[invoice.Id] and sc.[fds] = 0
|
|
||||||
LEFT JOIN contra_accounts as ca ON ISNULL(vat.[vat], 19.0) = ca.[vat]
|
|
||||||
LEFT JOIN [dbo].[mfr__*PartnerSet] as ps ON iv.[Id] = ps.[EntityId] AND ps.[Property] = 'Invoice:Customer'
|
|
||||||
where TRY_CAST(iv.id as varchar(50)) in (SELECT [Id] FROM @inv Where fds = 0)
|
|
||||||
UNION
|
|
||||||
select
|
|
||||||
iv.[Id]
|
|
||||||
,[InvoiceId] = iv.[InvoiceId] + ISNULL((CASE WHEN iv.InvoiceType = 'c' THEN (SELECT TOP(1) ' >> ' + i2.[InvoiceId] FROM [dbo].[fds__invoices] as i2 JOIN [dbo].[fds__invoice_details] as id2 on i2.[Id] = id2.[StornoTo_InvId] where id2.[InvId] = iv.[id] and i2.[IsFinal] = 1) ELSE '' END), '')
|
|
||||||
,[InvoiceType]
|
|
||||||
,[DocumentName]
|
|
||||||
,[requestcount] = sc.[#]
|
|
||||||
,[request] = (SELECT TOP(1) s.[ExternalId] FROM @SReq as s WHERE s.[invoice.Id] = iv.id)
|
|
||||||
,[balance] = FORMAT([invoiceBalance], '#0.00', 'de') + ' €'
|
|
||||||
,[EINZELPOS_brutto] = NULL --FORMAT(itm.bo_itemsum, '#0.00', 'de') + ' €'
|
|
||||||
,[EINZELPOS_netto] = NULL --FORMAT(itm.net_itemsum, '#0.00', 'de') + ' €'
|
|
||||||
,[vat] = FORMAT(ISNULL(iv.[InvoiceVAT_1], 19.0), '#0.0', 'de') + ' %'
|
|
||||||
,[deb_cred] = CASE WHEN [invoiceBalance] >= 0 THEN 'S' ELSE 'H' END
|
|
||||||
,[customer] = (SELECT TOP(1) [customer.ExternalId] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id] and [customer.ExternalId] is not null ORDER BY SReq.DateOfCreation ASC)
|
|
||||||
,[contra_account] = ca.[account]
|
|
||||||
,[Belegdatum] = FORMAT(iv.[DateCreated], 'dd.MM.yy')
|
|
||||||
,[Buchungstext] = (SELECT TOP(1) SReq.[name] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id])
|
|
||||||
,[DateOfCreation] = iv.[DateCreated]
|
|
||||||
,[DueDate]
|
|
||||||
,[PaymentTerm] = ISNULL([PaymentTerm], @stage1)
|
|
||||||
,[PaymentStatus]
|
|
||||||
,[IsPayed] = CAST(CASE WHEN ISNULL(iv.[IsCanceled],0) = 1 THEN 1 ELSE ISNULL([IsPayed],0) END as bit)
|
|
||||||
,[IsCanceled] = CAST(IIF(ISNULL([IsCanceled],0) = 1 OR [dbo].[fds__fn_isInvoiceCancelled](iv.[Id]) = 1, 1,0) as bit)
|
|
||||||
,[isSent]
|
|
||||||
,[isFinal]
|
|
||||||
,[hasFile] = CAST( CASE wHEN [file] is not null and [DocumentNAme] is not null THEN 1 ELSE 0 END as bit)
|
|
||||||
,[fds] = @true
|
|
||||||
,[customer_id] = iv.[CustomerId]
|
|
||||||
from [dbo].[fds__invoices] as iv
|
|
||||||
LEFT JOIN itm on iv.Id = itm.[invoice.Id]
|
|
||||||
LEFT JOIN sc on iv.[id] = sc.[invoice.Id] and sc.[fds] = 1
|
|
||||||
LEFT JOIN contra_accounts as ca ON ISNULL(iv.[InvoiceVAT_1], 19.0) = ca.[vat]
|
|
||||||
where iv.Id in (SELECT [Id] FROM @inv Where fds = 1)
|
|
||||||
), reminder as (
|
|
||||||
SELECT [InvId], [lastReminder] = MAX([DateFinalized]), [#] = COUNT(*) FROM [dbo].[fds__reminder] WHERE [isFinal] = 1 AND [invId] IN (SELECT [Id] FROM @inv) GROUP BY [InvId]
|
|
||||||
)
|
|
||||||
select
|
|
||||||
inv.*
|
|
||||||
,[reminderstatus] = CASE WHEN [ispayed] = 1 THEN 0
|
|
||||||
WHEN [iscanceled] = 1 THEN 0
|
|
||||||
WHEN DueDate is null THEN 0
|
|
||||||
WHEN [deb_cred] = 'H' THEN 0
|
|
||||||
WHEN @stage3 <> '' AND rem.[#] = 2 AND @today > [dbo].[date_addcustom]([lastReminder], @stage3) THEN 3
|
|
||||||
WHEN @stage2 <> '' AND rem.[#] = 1 AND @today > [dbo].[date_addcustom]([lastReminder], @stage2) THEN 2
|
|
||||||
WHEN @stage1 <> '' AND ISNULL(rem.[#],0) = 0 AND @today > [dbo].[date_addcustom](ISNULL(DueDate, [DateOfCreation]), ISNULL([PaymentTerm],@stage1)) THEN 1
|
|
||||||
ELSE 0 END
|
|
||||||
,[invstatus] = CASE WHEN [IsPayed] = 1 AND [isCanceled] = 0 THEN 'pyd'
|
|
||||||
WHEN [isCanceled] = 1 THEN 'cc'
|
|
||||||
WHEN [isFinal] = 0 THEN 'dft'
|
|
||||||
WHEN [isFinal] = 1 AND [isSent] = 0 THEN 'uns'
|
|
||||||
WHEN [DueDate] is null THEN 'op'
|
|
||||||
WHEN [deb_cred] = 'H' THEN ''
|
|
||||||
WHEN @today >= [DueDate] THEN 'due'
|
|
||||||
WHEN @stage1 <> '' AND ISNULL(rem.[#],0) = 0 AND @today > [dbo].[date_addcustom](ISNULL(DueDate, [DateOfCreation]), @stage1) THEN 'ovd'
|
|
||||||
WHEN ISNULL(rem.[#],0) > 0 THEN 'rem'
|
|
||||||
END
|
|
||||||
,[reminder] = rem.[#]
|
|
||||||
,[CustomerName] = c.[name]
|
|
||||||
,[order] = CASE WHEN @include_drafts = 1 THEN
|
|
||||||
ROW_NUMBER() OVER (ORDER BY inv.[DateOfCreation] ASC)
|
|
||||||
ELSE
|
|
||||||
ROW_NUMBER() OVER (ORDER BY TRY_CAST(SUBSTRING([invoiceid], 2,4) AS int), TRY_CAST(SUBSTRING([invoiceid], 7,4) AS int), inv.[DateOfCreation] ASC)
|
|
||||||
END
|
|
||||||
,[Payment] = IIF(py.[amount] is null,'',FORMAT(ISNULL(py.[amount],0.0), '0.00€', 'de') + IIF(py.[manu] = 1, ' man.',''))
|
|
||||||
/*CASE WHEN [paymentstatus] = 'a' THEN 'vollst.'
|
|
||||||
WHEN [paymentstatus] = 'p' THEN FORMAT(ISNULL([dbo].[fds__fn_InvoicePaymentAmount](inv.[Id]),0.0), '0.00€', 'de')
|
|
||||||
ELSE '' END*/
|
|
||||||
FROM inv
|
|
||||||
left JOIN reminder as rem ON inv.[Id] = rem.[InvId] --inv.[id] is already converted to varchar
|
|
||||||
LEFT JOIN [dbo].[mfr__companies] as c ON inv.[customer_id] = c.[id]
|
|
||||||
LEFT JOIN [dbo].[fds__fn_bankingtransactions_perInvoice](@startdate, @enddate) as py ON py.[invid] = inv.[Id]
|
|
||||||
WHERE ((@only_reminded = 0 AND @only_remindedunpayed = 0) OR ISNULL(rem.[#],0) > 0)
|
|
||||||
AND (CASE WHEN @only_unpayed = 0 AND @only_remindedunpayed = 0 THEN 1 WHEN [IsCanceled] = 1 THEN 0 ELSE 1 END) = 1
|
|
||||||
order by inv.[DateOfCreation] ASC;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
END
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
|
|
||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__getReminder]
|
|
||||||
@Id varchar(8),
|
|
||||||
@includefile bit,
|
|
||||||
@authuser varchar(25)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
IF [dbo].[fis_getModuleAuth]('fds_reminder', @authuser) < 1
|
|
||||||
THROW 60000, N'not authorized', 1;
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @rem [dbo].[fds__tt__reminder_core];
|
|
||||||
|
|
||||||
|
|
||||||
INSERT INTO @rem
|
|
||||||
SELECT TOP(1) [Id]
|
|
||||||
,[Version]
|
|
||||||
,[DocumentName]
|
|
||||||
,[InvId]
|
|
||||||
,[CustomerId]
|
|
||||||
,[SendToAddress]
|
|
||||||
,[SendToEmail]
|
|
||||||
,[type]
|
|
||||||
,[amount]
|
|
||||||
,[amount_payed]
|
|
||||||
,[amount_open]
|
|
||||||
,[subject]
|
|
||||||
,[text]
|
|
||||||
,[IsSent]
|
|
||||||
,[IsFinal]
|
|
||||||
,[CustomValues]
|
|
||||||
,[DateSent]
|
|
||||||
,[UserSent]
|
|
||||||
,[DateFinalized]
|
|
||||||
,[UserFinalized]
|
|
||||||
,[DateCreated]
|
|
||||||
,[UserCreated]
|
|
||||||
,[DateModified]
|
|
||||||
,[UserModified]
|
|
||||||
,[file] = CASE WHEN ISNULL(@includefile,0) = 1 THEN [file] else NULL END -- do not return file if not explicitly requested
|
|
||||||
FROM [dbo].[fds__reminder]
|
|
||||||
WHERE [Id] = @Id AND @Id is not null;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @InvId varchar(15) = (SELECT TOP(1) [InvId] fROM @rem);
|
|
||||||
|
|
||||||
---output
|
|
||||||
|
|
||||||
--admin
|
|
||||||
SELECT TOP(1)
|
|
||||||
[today]
|
|
||||||
, [sender] = N'Sebastian Fuchs GmbH & Co. KG ● Germaniastraße 15 ● 40223 Düsseldorf'
|
|
||||||
, [CustomerId] = rem.[CustomerId]
|
|
||||||
, [type] = rem.[type]
|
|
||||||
FROM (VALUES(CAST(GETDATE() as date)))base ([today]) cross JOIN @rem as rem;
|
|
||||||
|
|
||||||
--rem
|
|
||||||
With inv1 as(
|
|
||||||
select TOP(1) * FROM [dbo].[fds__invoices] WHERE [id] = @InvId
|
|
||||||
), inv as(
|
|
||||||
SELECT [Id] = cast([id] as varchar(15))
|
|
||||||
,[Invoiceid]
|
|
||||||
,[DateFinalized]
|
|
||||||
,[InvoiceBalance]
|
|
||||||
,[DocumentName]
|
|
||||||
,[file]
|
|
||||||
FROM inv1
|
|
||||||
UNION
|
|
||||||
SELECT [Id] = cast([id] as varchar(15))
|
|
||||||
,[Invoiceid]
|
|
||||||
,[DateFinalized] = [DateOfCreation]
|
|
||||||
,[InvoiceBalance]
|
|
||||||
,[DocumentName]
|
|
||||||
,[file] = NULL
|
|
||||||
FROM [dbo].[mfr__invoices] as mfri
|
|
||||||
WHERE [id] = TRY_CAST(@invId as bigint) AND NOT EXISTS( SELECT 0 FROM inv1 ) --will only be used, if id not present in fds__invoices
|
|
||||||
)
|
|
||||||
SELECT TOP(1) o.*
|
|
||||||
,inv.[InvoiceId]
|
|
||||||
,[InvoiceDate] = inv.[DateFinalized]
|
|
||||||
,[InvoiceFileName] = inv.[DocumentName]
|
|
||||||
,[InvoiceFile] = CASE WHEN ISNULL(@includefile,0) = 1 THEN inv.[file] else NULL END -- do not return file if not explicitly requested
|
|
||||||
,[hasFile] = CAST ( CASE WHEN o.[file] is null THEN 0 ELSE 1 END as bit)
|
|
||||||
,[UserNameFinalized] = [dbo].[fis_admin_getUserName_byID](o.[UserFinalized])
|
|
||||||
,[UserEmailFinalized] = [dbo].[fis_admin_getUserEmail_byID](o.[UserFinalized])
|
|
||||||
from @rem as o
|
|
||||||
join inv on o.[invid] = inv.[id];
|
|
||||||
|
|
||||||
END
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__getReportDocument]
|
|
||||||
|
|
||||||
@reportid varchar(20)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
|
|
||||||
DECLARE @idbigint bigint = TRY_CAST(@reportid as bigint);
|
|
||||||
|
|
||||||
SELECT TOP(1)
|
|
||||||
[Id] = CAST([Id] as varchar(20)),[InvoiceID],[URI],[DocumentName],[isInvoice] = 1,[DateOfCreation],[fds] = CAST( 0 as bit),[file] = CAST( null as varbinary(max))
|
|
||||||
,[order] = ROW_NUMBER() OVER (ORDER BY [DateOfCreation] ASC)
|
|
||||||
FROM [dbo].[mfr__invoices] WHERE [id] = @idbigint and @idbigint is not null
|
|
||||||
UNION
|
|
||||||
SELECT TOP(1)
|
|
||||||
[Id] = CAST([Id] as varchar(20)),[InvoiceID],[URI] = '',[DocumentName],[isInvoice] = 1,[DateOfCreation] = [DateCreated],[fds] = CAST( 1 as bit), [file]
|
|
||||||
,[order] = ROW_NUMBER() OVER (ORDER BY [DateCreated] ASC)
|
|
||||||
FROM [dbo].[fds__invoices] WHERE [id] = @reportid AND [isFinal] = 1;
|
|
||||||
;
|
|
||||||
|
|
||||||
END
|
|
||||||
@@ -1,422 +0,0 @@
|
|||||||
|
|
||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__getRequest_details]
|
|
||||||
@servicerequestid bigint
|
|
||||||
, @mode varchar(3)
|
|
||||||
, @authuser varchar(100)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
|
|
||||||
IF [dbo].[fis_getModuleAuth]('fds_req', @authuser) < 1
|
|
||||||
THROW 60000, N'not authorized', 1;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @today date = GETDATE();
|
|
||||||
DECLARE @SReq TABLE (
|
|
||||||
[Id] [bigint] NOT NULL,
|
|
||||||
[Name] [nvarchar](255) NULL,
|
|
||||||
[ExternalId] [nvarchar](255) NULL,
|
|
||||||
[InvoiceId] [nvarchar](255) NULL,
|
|
||||||
[ClosedAt] [datetime] NULL,
|
|
||||||
[ReleasedAt] [datetime] NULL,
|
|
||||||
[WorkDoneAt] [datetime] NULL,
|
|
||||||
[TargetTimeInMinutes] [bigint] NOT NULL,
|
|
||||||
[DateModified] [datetime] NULL,
|
|
||||||
[DateOfCreation] [datetime] NULL,
|
|
||||||
[DueDateRangeStart] [datetime] NULL,
|
|
||||||
[DueDateRangeEnd] [datetime] NULL,
|
|
||||||
[PortalLink] [nvarchar](255) NULL,
|
|
||||||
[CostCenterId] [bigint] NULL,
|
|
||||||
[Description] [nvarchar](max) NULL,
|
|
||||||
[State] [nvarchar](255) NULL,
|
|
||||||
[CustomValues] [nvarchar](max) NOT NULL,
|
|
||||||
[CurrentOwnerId] [bigint] NULL,
|
|
||||||
[CustomerId] [bigint] NULL,
|
|
||||||
[ParentServiceRequestId] [bigint] NULL,
|
|
||||||
[ParentExtenalId] [nvarchar](255) NULL,
|
|
||||||
[ParentWorkDoneAt] [date] NULL,
|
|
||||||
[ParentInvoiceId] [nvarchar](255) NULL,
|
|
||||||
[Location#ID] [bigint] NULL,
|
|
||||||
[Version] [int] NOT NULL,
|
|
||||||
[IsTemplate] [bit] NOT NULL,
|
|
||||||
[IsTemplateMobile] [bit] NOT NULL,
|
|
||||||
[CreateFromServiceRequestTemplateId] [bigint] NOT NULL,
|
|
||||||
[Type] [nvarchar](255) NULL,
|
|
||||||
[Summary] nvarchar(255) NULL,
|
|
||||||
[isChild] bit NULL,
|
|
||||||
[baseorder] int,
|
|
||||||
[provision] varchar(255) NULL,
|
|
||||||
[open] bit NULL
|
|
||||||
);
|
|
||||||
insert into @SReq
|
|
||||||
SELECT
|
|
||||||
[Id]
|
|
||||||
,[Name]
|
|
||||||
,[ExternalId]
|
|
||||||
,[InvoiceId]
|
|
||||||
,[ClosedAt]
|
|
||||||
,[ReleasedAt]
|
|
||||||
,[WorkDoneAt]
|
|
||||||
,[TargetTimeInMinutes]
|
|
||||||
,[DateModified]
|
|
||||||
,[DateOfCreation]
|
|
||||||
,[DueDateRangeStart]
|
|
||||||
,[DueDateRangeEnd]
|
|
||||||
,[PortalLink]
|
|
||||||
,[CostCenterId]
|
|
||||||
,[Description]
|
|
||||||
,[State]
|
|
||||||
,[CustomValues]
|
|
||||||
,[CurrentOwnerId]
|
|
||||||
,[CustomerId]
|
|
||||||
,[ParentServiceRequestId] = CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then [id] ELSE [ParentServiceRequestId] END
|
|
||||||
,[ParentExtenalId] = cast(CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then null ELSE '?' END as nvarchar(255))
|
|
||||||
,[ParentWorkDoneAt] = cast(NULL as datetime)
|
|
||||||
,[ParentInvoiceId] = cast(NULL as nvarchar(255))
|
|
||||||
,[Location#ID]
|
|
||||||
,[Version]
|
|
||||||
,[IsTemplate]
|
|
||||||
,[IsTemplateMobile]
|
|
||||||
,[CreateFromServiceRequestTemplateId]
|
|
||||||
,[Type]
|
|
||||||
,[Summary] = NULL
|
|
||||||
,[isChild] = CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then 0 ELSE 1 END
|
|
||||||
,[baseorder] = 0
|
|
||||||
,[provision] = NULL
|
|
||||||
,[open] = 1
|
|
||||||
FROM [dbo].[mfr__servicerequests] as s
|
|
||||||
WHERE [id] = @servicerequestid;
|
|
||||||
|
|
||||||
INSERT INTO @SReq
|
|
||||||
SELECT s.[Id]
|
|
||||||
,s.[Name]
|
|
||||||
,s.[ExternalId]
|
|
||||||
,s.[InvoiceId]
|
|
||||||
,s.[ClosedAt]
|
|
||||||
,s.[ReleasedAt]
|
|
||||||
,s.[WorkDoneAt]
|
|
||||||
,s.[TargetTimeInMinutes]
|
|
||||||
,s.[DateModified]
|
|
||||||
,s.[DateOfCreation]
|
|
||||||
,s.[DueDateRangeStart]
|
|
||||||
,s.[DueDateRangeEnd]
|
|
||||||
,s.[PortalLink]
|
|
||||||
,s.[CostCenterId]
|
|
||||||
,s.[Description]
|
|
||||||
,s.[State]
|
|
||||||
,s.[CustomValues]
|
|
||||||
,s.[CurrentOwnerId]
|
|
||||||
,s.[CustomerId]
|
|
||||||
,s.[ParentServiceRequestId]
|
|
||||||
,[ParentExtenalId] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[ExternalId] ELSE NULL END
|
|
||||||
,[ParentWorkDoneAt] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[WorkDoneAt] ELSE NULL END
|
|
||||||
,[ParentInvoiceId] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[InvoiceId] ELSE NULL END
|
|
||||||
,s.[Location#ID]
|
|
||||||
,s.[Version]
|
|
||||||
,s.[IsTemplate]
|
|
||||||
,s.[IsTemplateMobile]
|
|
||||||
,s.[CreateFromServiceRequestTemplateId]
|
|
||||||
,s.[Type]
|
|
||||||
,[Summary] = NULL
|
|
||||||
,[isChild] = CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then 0 ELSE 1 END
|
|
||||||
,[baseorder] = 0
|
|
||||||
, [provision] = null
|
|
||||||
,[open] = 1
|
|
||||||
from [dbo].[mfr__servicerequests] as s JOIN @SReq as q ON (s.[ParentServiceRequestId] = q.[id] OR q.[ParentServiceRequestId] = s.[id]) --parents or childs
|
|
||||||
WHERE NOT EXISTS (SELECT * FROM @SReq as s2 WHERE s2.[id] = s.[Id])
|
|
||||||
;
|
|
||||||
|
|
||||||
-- need to use CTE, because row_number is not available with update statements
|
|
||||||
WITH q as (
|
|
||||||
SELECT [id], [baseorder] = (ROW_NUMBER() OVER (ORDER BY ISNULL([ParentWorkDoneAt], [WorkDoneAt]), ISNULL([ParentServiceRequestId],[id])) * 100)
|
|
||||||
FROM @SReq as s
|
|
||||||
WHERE [ischild] = 0
|
|
||||||
)
|
|
||||||
UPDATE s SET
|
|
||||||
[baseorder] = q.baseorder
|
|
||||||
FROM @SReq as s JOIN q ON s.[id] = q.[id];
|
|
||||||
|
|
||||||
|
|
||||||
-- need to use CTE, because row_number is not available with update statements
|
|
||||||
WITH q as (
|
|
||||||
SELECT s.[id]
|
|
||||||
, q.[ExternalId], q.[WorkDoneAt], q.[InvoiceId]
|
|
||||||
, [baseorder] = q.[baseorder] + ROW_NUMBER() OVER (PARTITION BY q.[id] ORDER BY s.[dateofcreation])
|
|
||||||
FROM @SReq as s JOIN @SReq as q ON s.[ParentServiceRequestId] = q.[id]
|
|
||||||
WHERE s.[ischild] = 1
|
|
||||||
)
|
|
||||||
UPDATE s SET
|
|
||||||
[ParentExtenalId] = q.[ExternalId]
|
|
||||||
,[ParentWorkDoneAt] = q.[WorkDoneAt]
|
|
||||||
,[ParentInvoiceId] = q.[InvoiceId]
|
|
||||||
,[baseorder] = q.[baseorder]
|
|
||||||
FROM @SReq as s JOIN q ON s.[Id] = q.[id];
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @inv TABLE ([Id] bigint, [Invoices] varchar(1000), [isOpen] bit);
|
|
||||||
With fdsinv as(
|
|
||||||
SELECT [Id] = fdsr.[mfr__servicerequest], fdsi.[InvoiceId], fdsi.[InvoiceType], [isCanceled] = ISNULL(fdsi.[isCanceled],0)
|
|
||||||
FROM [dbo].[fds__invoice_servicerequests] as fdsr
|
|
||||||
JOIN [dbo].[fds__invoices] as fdsi on fdsr.[invid] = fdsi.[id]
|
|
||||||
JOIN @SReq as s ON fdsr.[mfr__servicerequest] = s.[id]
|
|
||||||
WHERE [isFinal] = 1 AND fdsr.[mfr__servicerequest] is not null
|
|
||||||
|
|
||||||
), fi as(
|
|
||||||
SELECT [Id], [isOpen] = MIN(CASE WHEN ISNULL([invoicetype],'r') = 'r' AND [isCanceled] = 0 THEN 0 ELSE 1 END) FROM fdsinv GROUP BY [Id]
|
|
||||||
)
|
|
||||||
INSERT INTO @inv
|
|
||||||
SELECT [Id]
|
|
||||||
, [invoices] = ISNULL(STUFF( (SELECT CHAR(10) + [InvoiceId] + ' (' + [InvoiceTYpe] + ')' FROM fdsinv as f2 WHERE f2.[id] = fi.[Id] FOR XML PATH, TYPE).value('.[1]','nvarchar(max)') ,1,1,'') , '')
|
|
||||||
, [isOpen]
|
|
||||||
FROM fi;
|
|
||||||
|
|
||||||
UPDATE s
|
|
||||||
SET s.[invoiceid] = [dbo].[fds__fn_InvoiceIDs_mfr](s.[invoiceid], inv.[Invoices])
|
|
||||||
,[open] = 1 --CASE WHEN s.[workdoneat] is not null AND [dbo].[fds__fn_InvoiceIDs_mfr](s.[invoiceid],'') = '' AND ISNULL(inv.[isOpen],1) = 1 THEN 1 ELSE 0 END
|
|
||||||
FROM @SReq as s LEFT JOIN @inv as inv on s.[id] = inv.[id];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @step TABLE (
|
|
||||||
[Id] [bigint] NOT NULL,
|
|
||||||
[MobileId] [bigint] NULL,
|
|
||||||
[Version] [int] NOT NULL,
|
|
||||||
[Name] [nvarchar](255) NULL,
|
|
||||||
[IsDone] [bit] NOT NULL,
|
|
||||||
[HasError] [bit] NOT NULL,
|
|
||||||
[TrackingId] [nvarchar](255) NULL,
|
|
||||||
[Type] [nvarchar](255) NULL,
|
|
||||||
[SortOrder] [int] NOT NULL,
|
|
||||||
[Data] [nvarchar](max) NULL,
|
|
||||||
[DateModifiedOffline] [datetime] NULL,
|
|
||||||
[ServiceRequestId] [bigint] NULL,
|
|
||||||
[Description] [nvarchar](max) NULL,
|
|
||||||
[Comment] [nvarchar](max) NULL,
|
|
||||||
[InternalComment] [nvarchar](max) NULL,
|
|
||||||
[ServiceObjectId] [bigint] NULL,
|
|
||||||
[StepListTemplateId] [bigint] NULL,
|
|
||||||
[ParentId] [bigint] NULL,
|
|
||||||
[order] int NOT NULL
|
|
||||||
);
|
|
||||||
DECLARE @company TABLE (
|
|
||||||
[Id] [bigint] NOT NULL,
|
|
||||||
[Location#ID] [bigint] NULL,
|
|
||||||
[Version] [int] NOT NULL,
|
|
||||||
[IsPhysicalPerson] [bit] NOT NULL,
|
|
||||||
[IsOwner] [bit] NOT NULL,
|
|
||||||
[IsEmailInvoicingActive] [bit] NOT NULL,
|
|
||||||
[IsSupplier] [bit] NOT NULL,
|
|
||||||
[MappingId] [nvarchar](255) NULL,
|
|
||||||
[ExternalId] [nvarchar](255) NULL,
|
|
||||||
[Name] [nvarchar](255) NULL,
|
|
||||||
[Note] [nvarchar](255) NULL,
|
|
||||||
[SupportTelephone] [nvarchar](255) NULL,
|
|
||||||
[SupportFax] [nvarchar](255) NULL,
|
|
||||||
[SupportMail] [nvarchar](255) NULL,
|
|
||||||
[QuickSearch] [nvarchar](255) NULL,
|
|
||||||
[DateModified] [datetime] NOT NULL,
|
|
||||||
[CustomValues] [nvarchar](max) NOT NULL,
|
|
||||||
[MainContactId] [bigint] NOT NULL,
|
|
||||||
[address] nvarchar(1000) null
|
|
||||||
);
|
|
||||||
INSERT INTO @company SELECT DISTINCT cy.*, [address] = [dbo].[fds__getCompanyAddress](cy.[id]) FROM [mfr__companies] as cy JOIN @SReq as s on cy.id = s.[CustomerId];
|
|
||||||
DECLARE @companyids [dbo].[fds__tt__idlist];
|
|
||||||
INSERT INTO @companyids SELECT DISTINCT [id] FROM @company;
|
|
||||||
|
|
||||||
/** OUTPUT + get other types **/
|
|
||||||
|
|
||||||
IF @mode in ('r','ov', '','f', 'ful')
|
|
||||||
BEGIN
|
|
||||||
|
|
||||||
WITH app as(
|
|
||||||
SELECT [start] = MIN(CAST(CASE WHEN a.[StartDateTime] is null THEN s.[workdoneat] WHEN CAST(a.[StartDateTime] as date) > s.[workdoneat] THEN CAST(a.[StartDateTime] as date) ELSE s.[workdoneat] END as date))
|
|
||||||
, [end] = MAX(CAST(CASE WHEN a.[EndDateTime] is null THEN s.[workdoneat] WHEN CAST(a.[EndDateTime] as date) > s.[workdoneat] THEN CAST(a.[EndDateTime] as date) ELSE s.[workdoneat] END as date))
|
|
||||||
, [CustomerId] = MAX (s.customerid)
|
|
||||||
, [ParentServiceRequestId] = ISNULL(MIN(CASE WHEN [isChild] = 1 then null else s.[Id] END), MAX([ParentServiceRequestId])) --make sure only one returned
|
|
||||||
FROM [dbo].[mfr__appointments] as a
|
|
||||||
RIGHT JOIN @SReq as s on a.[ServiceRequestId] = s.[Id] AND a.[State] in ('IsWorkDone')
|
|
||||||
)
|
|
||||||
SELECT TOP(1)
|
|
||||||
[today]
|
|
||||||
, [provisionstart] = CASe WHEN [start] = [end] THEN NULL ELSE [start] END
|
|
||||||
, [provisionend] = [end]
|
|
||||||
, [sender] = N'Sebastian Fuchs GmbH & Co. KG ● Germaniastraße 15 ● 40223 Düsseldorf'
|
|
||||||
, [paymentterms] = N'10wd'
|
|
||||||
, [invoiceemail] = (SELECT TOP(1) [SupportMail] FROM @company where IsEmailInvoicingActive = 1)
|
|
||||||
, [invoiceaddress] = (SELECT TOP(1) CONCAT([name], CHAR(10), [address]) FROM @company ORDER BY IsEmailInvoicingActive DESC)
|
|
||||||
, [tax_servicerefund] = 0.2
|
|
||||||
, [CustomerId] = [CustomerId]
|
|
||||||
, [search] = @servicerequestid
|
|
||||||
, [parent] = app.[ParentServiceRequestId]
|
|
||||||
FROM (VALUES(CAST(GETDATE() as date)))base ([today]) CROSS JOIN app;
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
*
|
|
||||||
,[order] = ROW_NUMBER() OVER (ORDER BY [baseorder])
|
|
||||||
FROM @SReq;
|
|
||||||
END
|
|
||||||
|
|
||||||
IF @mode in ('i','ov')
|
|
||||||
BEGIN
|
|
||||||
SELECT
|
|
||||||
itm.[Id]
|
|
||||||
, [net_pos] = CASE WHEN LOWER(itm.[type]) in ('Title','Text') THEN '' ELSE FORMAT( CAST( (ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0) as numeric(7,2)) , '#0.00 €','de') END
|
|
||||||
, [bo_pos] = CASE WHEN LOWER(itm.[type]) in ('Title','Text') THEN '' ELSE FORMAT( CAST( ((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) * (1 + (ISNULL(vat.[vat], 19.0) * 0.01)) as numeric(7,2)) , '#0.00 €','de') END
|
|
||||||
, [vat] = CASE WHEN LOWER(itm.[type]) in ('Title','Text') THEN null ELSE FORMAT(ISNULL(vat.[vat], 19.0), '#0.0','de') + '%' END
|
|
||||||
, itm.[ServiceRequestId]
|
|
||||||
, itm.[SortOrder]
|
|
||||||
, itm.[Type]
|
|
||||||
, itm.[NameOrNumber]
|
|
||||||
, itm.[Note]
|
|
||||||
, [order] = (s.[baseorder] * 100) + ROW_NUMBER() OVER (PARTITION BY itm.[ServiceRequestId] ORDER BY itm.[SortOrder])
|
|
||||||
FROM [dbo].[mfr__items] as itm
|
|
||||||
JOIN @SReq as s ON itm.ServiceRequestId = s.[Id]
|
|
||||||
LEFT JOIN [dbo].[fds__custom_vat] as vat ON itm.[Id] = vat.[EntityId] and vat.[EntityType] = 'item'
|
|
||||||
;
|
|
||||||
END
|
|
||||||
ELSE IF @mode in ('f', 'ful')
|
|
||||||
BEGIN
|
|
||||||
SELECT
|
|
||||||
itm.[Id]
|
|
||||||
, [net] = ISNULL([Price],0)
|
|
||||||
, [quantityhours] = ISNULL([quantityhours],1)
|
|
||||||
, itm.[UnitString]
|
|
||||||
, [net_val] = CAST( (ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0) as numeric(7,2))
|
|
||||||
, [bo_val] = CAST( ((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) * (1 + (ISNULL(vat.[vat], 19.0) * 0.01)) as numeric(7,2))
|
|
||||||
, [vat] = CASE WHEN LOWER(itm.[type]) in ('Title','Text') THEN null ELSE FORMAT(ISNULL(vat.[vat], 19.0), '#0.0','de') + '%' END
|
|
||||||
, [vat_val] = CAST( ((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) * ((ISNULL(vat.[vat], 19.0) * 0.01)) as numeric(7,2))
|
|
||||||
, itm.[ServiceRequestId]
|
|
||||||
, itm.[SortOrder]
|
|
||||||
, itm.[Type]
|
|
||||||
, itm.[NameOrNumber]
|
|
||||||
, itm.[Note]
|
|
||||||
, [order] = (s.[baseorder] * 100) + ROW_NUMBER() OVER (PARTITION BY itm.[ServiceRequestId] ORDER BY itm.[SortOrder], itm.[Id])
|
|
||||||
FROM [dbo].[mfr__items] as itm
|
|
||||||
JOIN @SReq as s ON itm.ServiceRequestId = s.[Id]
|
|
||||||
LEFT JOIN [dbo].[fds__custom_vat] as vat ON itm.[Id] = vat.[EntityId] and vat.[EntityType] = 'item'
|
|
||||||
;
|
|
||||||
END
|
|
||||||
|
|
||||||
IF @mode in ('f', 'ful')
|
|
||||||
BEGIN
|
|
||||||
INSERT INTO @step
|
|
||||||
SELECT
|
|
||||||
stp.*
|
|
||||||
, [order] = (s.[baseorder] * 100) + ROW_NUMBER() OVER (PARTITION BY stp.[ServiceRequestId] ORDER BY stp.[SortOrder], stp.[Id])
|
|
||||||
FROM [dbo].[mfr__steps] as stp JOIN @SReq as s ON stp.ServiceRequestId = s.[Id];
|
|
||||||
SELECT * from @step;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
cc.[Id]
|
|
||||||
, [CompanyName] = cc.[name]
|
|
||||||
, ct.[Gender]
|
|
||||||
, ct.[FirstName]
|
|
||||||
, ct.[LastName]
|
|
||||||
, ct.[Email]
|
|
||||||
, cc.[address]
|
|
||||||
FROM [dbo].[fds__getCompanysContacts](@companyids) as ct join @company as cc on cc.[Id] = ct.[CyId];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
SELECT DISTINCT
|
|
||||||
[ref] = 'Einsatzort ' + so.[name]
|
|
||||||
,[address] = [dbo].[fds__fn_combineAddress] (so.[Name], loc.[AddressString], null
|
|
||||||
,loc.[Postal]
|
|
||||||
,loc.[City]
|
|
||||||
,loc.[State]
|
|
||||||
,loc.[Country])
|
|
||||||
FROM @SReq as s
|
|
||||||
LEFT JOIN [mfr__*PartnerSet] as p ON p.property = 'ServiceRequest:ServiceObjects' and p.[EntityId] = s.[id]
|
|
||||||
LEFT JOIN [mfr__serviceobjects] as so ON so.id = p.[PartnerId]
|
|
||||||
LEFT JOIN [mfr__*PartnerSet] as po ON p.property = 'ServiceObject:Location' and po.[EntityId] = so.[Id]
|
|
||||||
LEFT join [dbo].[mfr__#locations] as loc ON (loc.[id] = so.[Location#ID] OR po.[PartnerId] = loc.[Id] OR (so.[Id] = loc.[EntityId] and loc.Property = 'ServiceObject:Location'))
|
|
||||||
WHERE so.id is not null and loc.[id] is not null
|
|
||||||
UNION
|
|
||||||
SELECT DISTINCT
|
|
||||||
[ref] = 'Auftragsort ' + [name]
|
|
||||||
,[address] = [dbo].[fds__fn_combineAddress] (loc.[AddressString], null, null
|
|
||||||
,loc.[Postal]
|
|
||||||
,loc.[City]
|
|
||||||
,loc.[State]
|
|
||||||
,loc.[Country])
|
|
||||||
FROM @SReq as s
|
|
||||||
LEFT JOIN [mfr__*PartnerSet] as p ON p.property = 'ServiceRequest:Location' and p.[EntityId] = s.[id]
|
|
||||||
join [dbo].[mfr__#locations] as loc ON (s.[Location#ID] = loc.[id] OR p.[PartnerId] = loc.[Id] OR (s.[Id] = loc.[EntityId] and loc.Property = 'ServiceRequest:Location'))
|
|
||||||
WHERE loc.[id] is not null
|
|
||||||
UNION
|
|
||||||
SELECT DISTINCT [ref] = 'Kunde'
|
|
||||||
, [address]
|
|
||||||
FROM @company;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
END
|
|
||||||
|
|
||||||
|
|
||||||
--get invoices
|
|
||||||
|
|
||||||
--Reminder settings
|
|
||||||
DECLARE @stage1 varchar(10), @stage2 varchar(10), @stage3 varchar(10);
|
|
||||||
SELECT @stage1 = [stage1], @stage2 = [stage2], @stage3 = [stage3] FROM [dbo].[fds__admin_reminderSettings]();
|
|
||||||
With inv as(
|
|
||||||
SELECT [id] = CAST(ir.invid as varchar(20)), [isChild] = CAST(MIN(CAST(s.[isChild] as tinyint)) as BIT), [baseorder] = MIN(s.[baseorder]), [sopen] = CAST( MIN( CAST(s.[open] as tinyint) ) as bit)
|
|
||||||
FROM [dbo].[fds__invoice_servicerequests] as ir
|
|
||||||
JOIN @sreq as s ON ir.[mfr__servicerequest] = s.[id]
|
|
||||||
GROUP by ir.invid
|
|
||||||
UNION
|
|
||||||
SELECT [id] = CAST(i.[id] as varchar(20)), [isChild] = CAST(MIN(CAST(s.[isChild] as tinyint)) as BIT), [baseorder] = MIN(s.[baseorder]), [sopen] = CAST( MIN( CAST(s.[open] as tinyint) ) as bit)
|
|
||||||
FROM [dbo].[mfr__servicerequests] as ms JOIN @sreq as s on ms.[id] = s.[id]
|
|
||||||
JOIN [mfr__invoices] as i on ms.[InvoiceId] like ('%' + i.[InvoiceId] + '%')
|
|
||||||
GROUP by i.[id]
|
|
||||||
),reminder as (
|
|
||||||
SELECT [InvId], [lastReminder] = MAX([DateSent]), [#] = COUNT(*) FROM [dbo].[fds__reminder] WHERE [isSent] = 1 AND [invId] IN (SELECT [Id] FROM inv) GROUP BY [InvId]
|
|
||||||
)
|
|
||||||
SELECT [Id] = CAST(i.[Id] as varchar(20))
|
|
||||||
,i.[DateCreated]
|
|
||||||
,[isFinal]
|
|
||||||
,[isSent]
|
|
||||||
,i.[DocumentName]
|
|
||||||
,[invstatus] = CASE WHEN [IsPayed] = 1 AND [isCanceled] = 0 THEN 'pyd'
|
|
||||||
WHEN [isCanceled] = 1 OR [dbo].[fds__fn_isInvoiceCancelled](inv.[Id]) = 1 THEN 'cc'
|
|
||||||
WHEN [isFinal] = 0 THEN 'dft'
|
|
||||||
WHEN [isFinal] = 1 AND [isSent] = 0 THEN 'uns'
|
|
||||||
WHEN [DueDate] is null THEN 'op'
|
|
||||||
WHEN [invoiceBalance] < 0 THEN ''
|
|
||||||
WHEN @today >= [DueDate] THEN 'due'
|
|
||||||
WHEN @stage1 <> '' AND ISNULL(rem.[#],0) = 0 AND @today > [dbo].[date_addcustom](ISNULL(DueDate, i.[DateCreated]), @stage1) THEN 'ovd'
|
|
||||||
WHEN ISNULL(rem.[#],0) > 0 THEN 'rem'
|
|
||||||
END
|
|
||||||
,[order] = ROW_NUMBER() OVER ( ORDER BY inv.[isChild], inv.[baseorder], i.[isFinal] DESC, i.[datecreated] ASC )
|
|
||||||
FROM [dbo].[fds__invoices] as i
|
|
||||||
JOIN inv on i.[Id] = inv.[Id]
|
|
||||||
LEFT JOIN reminder as rem on inv.id = rem.[InvId]
|
|
||||||
WHERE (inv.[sopen] = 1 OR i.[isfinal] = 1) --- don't show any draft invoices for final tasks;
|
|
||||||
UNION
|
|
||||||
SELECT [Id] = CAST(i.[Id] as varchar(20))
|
|
||||||
,[DateCreated] = i.[DateOfCreation]
|
|
||||||
,[isFinal] = 1
|
|
||||||
,[isSent] = 1
|
|
||||||
,i.[DocumentName]
|
|
||||||
,[invstatus] = CASE WHEN [invoicestate] in ('eIsPaid') THEN 'pyd'
|
|
||||||
WHEN [invoicestate] in ('eIsCancelled') THEN 'cc'
|
|
||||||
--WHEN [isFinal] = 0 THEN 'dft'
|
|
||||||
--WHEN [isFinal] = 1 AND [isSent] = 0 THEN 'uns'
|
|
||||||
WHEN [DueDate] is null THEN 'op'
|
|
||||||
WHEN @today >= [DueDate] THEN 'due'
|
|
||||||
WHEN ISNULL(rem.[#],0) > 0 THEN 'rem'
|
|
||||||
WHEN @stage1 <> '' AND ISNULL(rem.[#],0) = 0 AND @today > [dbo].[date_addcustom](ISNULL(DueDate, i.[DateofCreation]), @stage1) THEN 'ovd'
|
|
||||||
END
|
|
||||||
,[order] = ROW_NUMBER() OVER ( ORDER BY inv.[isChild], inv.baseorder, i.[dateofcreation] ASC)
|
|
||||||
FROM [dbo].[mfr__invoices] as i
|
|
||||||
JOIN inv on TRY_CAST(i.[Id] as varchar(20)) = inv.[Id]
|
|
||||||
LEFT JOIN reminder as rem on inv.id = rem.[InvId];
|
|
||||||
END
|
|
||||||
@@ -1,278 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__getRequests_list]
|
|
||||||
@tgtdate date
|
|
||||||
,@mode varchar(1) = ''
|
|
||||||
,@completed bit = 1
|
|
||||||
,@authuser varchar(100)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
IF [dbo].[fis_getModuleAuth]('fds_req', @authuser) < 1
|
|
||||||
THROW 60000, N'not authorized', 1;
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info])
|
|
||||||
VALUES ('fds__getRequests_list' ,@authuser , '');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @startdate date, @enddate date, @today date = GETDATE();
|
|
||||||
SET @mode = ISNULL(LOWER(@mode),'');
|
|
||||||
If @mode in ('r') -- r = open recent
|
|
||||||
BEGIN
|
|
||||||
SET @tgtdate = @today;
|
|
||||||
SELECT @startdate = DATEADD(week, -4, [dbo].[date_weekfirst](@tgtdate))
|
|
||||||
,@enddate = @today;
|
|
||||||
END
|
|
||||||
else If @mode in ('m') -- r = open recent
|
|
||||||
BEGIN
|
|
||||||
SELECT @startdate = [dbo].[date_monthfirst](@tgtdate)
|
|
||||||
,@enddate = [dbo].[date_monthend](@tgtdate);
|
|
||||||
END
|
|
||||||
ELSE If @mode = 'w'
|
|
||||||
BEGIN
|
|
||||||
SELECT @startdate = [dbo].[date_weekfirst](@tgtdate)
|
|
||||||
,@enddate = [dbo].[date_weekend](@tgtdate);
|
|
||||||
END
|
|
||||||
|
|
||||||
DECLARE @openmode as bit = CASE WHEN LOWER(LEFT(ISNULL(@mode, ''),1)) IN ('o', 'r') THEN 1 ELSE 0 END;
|
|
||||||
SELECT [startdate] = @startdate, [enddate] = @enddate, [mode] = @mode
|
|
||||||
, [title] = CASE WHEN @openmode = 1 AND @mode = 'r' THEN 'Übersicht der offenen Aufträge (letzte 4 Wochen seit ' + FORMAT(@startdate, 'dd.MM.yyyy') + ')'
|
|
||||||
WHEN @openmode = 1 THEN 'Übersicht der offenen Aufträge'
|
|
||||||
ELSE 'Auftragsübersicht für ' + CASE WHEN @mode = 'm' THEN 'MONAT ' + FORMAT(@startdate, 'MMM yyyy')
|
|
||||||
WHEN @mode = 'w' THEN 'WOCHE ' + FORMAT(@startdate, 'dd.MM.yyyy') + ' - ' + FORMAT(@enddate, 'dd.MM.')
|
|
||||||
ELSE '' END
|
|
||||||
END
|
|
||||||
, [note] = CASE WHEN @openmode = 1 and @mode = 'r' THEN 'Es werden alle Aufträge angezeigt, die selbst offen sind und seit ' + FORMAT(@startdate, 'dd.MM.yyyy') + ' erstellt wurden, oder deren Folgeauftrag offen ist und/oder seit ' + FORMAT(@startdate, 'dd.MM.yyyy') + ' erstellt wurden.'
|
|
||||||
WHEN @openmode = 1 THEN 'Es werden alle Aufträge angezeigt, die selbst oder ein Folgeeauftrag offen ist.'
|
|
||||||
ELSE '' END;
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @SReq TABLE (
|
|
||||||
[Id] [bigint] NOT NULL,
|
|
||||||
[Name] [nvarchar](255) NULL,
|
|
||||||
[ExternalId] [nvarchar](255) NULL,
|
|
||||||
[InvoiceId] varchar(1000) NULL,
|
|
||||||
[ClosedAt] [datetime] NULL,
|
|
||||||
[ReleasedAt] [datetime] NULL,
|
|
||||||
[WorkDoneAt] [datetime] NULL,
|
|
||||||
[TargetTimeInMinutes] [bigint] NULL,
|
|
||||||
[DateModified] [datetime] NULL,
|
|
||||||
[DateOfCreation] [datetime] NULL,
|
|
||||||
[DueDateRangeStart] [datetime] NULL,
|
|
||||||
[DueDateRangeEnd] [datetime] NULL,
|
|
||||||
[PortalLink] [nvarchar](255) NULL,
|
|
||||||
[CostCenterId] [bigint] NULL,
|
|
||||||
[Description] [nvarchar](max) NULL,
|
|
||||||
[State] [nvarchar](255) NULL,
|
|
||||||
[CustomValues] [nvarchar](max) NULL,
|
|
||||||
[CurrentOwnerId] [bigint] NULL,
|
|
||||||
[CustomerId] [bigint] NULL,
|
|
||||||
[ParentServiceRequestId] [bigint] NULL,
|
|
||||||
[ParentExtenalId] [nvarchar](255) NULL,
|
|
||||||
[ParentWorkDoneAt] [date] NULL,
|
|
||||||
[ParentInvoiceId] varchar(1000) NULL,
|
|
||||||
[Location#ID] [bigint] NULL,
|
|
||||||
[Version] [int] NULL,
|
|
||||||
[IsTemplate] [bit] NULL,
|
|
||||||
[IsTemplateMobile] [bit] NULL,
|
|
||||||
[CreateFromServiceRequestTemplateId] [bigint] NULL,
|
|
||||||
[Type] [nvarchar](255) NULL,
|
|
||||||
[CustomerName] [nvarchar](255) NULL,
|
|
||||||
[isChild] bit NULL,
|
|
||||||
[open] bit,
|
|
||||||
[baseorder] int
|
|
||||||
);
|
|
||||||
DECLARE @inv TABLE ([Id] bigint, [Invoices] varchar(1000), [isOpen] bit);
|
|
||||||
With fdsinv as(
|
|
||||||
SELECT [Id] = fdsr.[mfr__servicerequest], fdsi.[InvoiceId], fdsi.[InvoiceType], [isCanceled] = ISNULL(fdsi.[isCanceled],0) FROM [dbo].[fds__invoice_servicerequests] as fdsr
|
|
||||||
JOIN [dbo].[fds__invoices] as fdsi on fdsr.[invid] = fdsi.[id]
|
|
||||||
WHERE [isFinal] = 1 AND fdsr.[mfr__servicerequest] is not null
|
|
||||||
), fi as(
|
|
||||||
SELECT [Id], [isOpen] = MIN(CASE WHEN ISNULL([invoicetype],'r') = 'r' AND [isCanceled] = 0 THEN 0 ELSE 1 END) FROM fdsinv GROUP BY [Id]
|
|
||||||
)
|
|
||||||
INSERT INTO @inv
|
|
||||||
SELECT [Id]
|
|
||||||
, [invoices] = ISNULL(STUFF( (SELECT CHAR(10) + [InvoiceId] + ' (' + [InvoiceTYpe] + ')' FROM fdsinv as f2 WHERE f2.[id] = fi.[Id] FOR XML PATH, TYPE).value('.[1]','nvarchar(max)') ,1,1,'') , '')
|
|
||||||
, [isOpen]
|
|
||||||
FROM fi;
|
|
||||||
|
|
||||||
|
|
||||||
insert into @SReq
|
|
||||||
SELECT
|
|
||||||
s.[Id]
|
|
||||||
,[Name]
|
|
||||||
,[ExternalId]
|
|
||||||
,[InvoiceId] = [dbo].[fds__fn_InvoiceIDs_mfr](s.[invoiceid], inv.[Invoices])
|
|
||||||
,[ClosedAt]
|
|
||||||
,[ReleasedAt]
|
|
||||||
,[WorkDoneAt]
|
|
||||||
,[TargetTimeInMinutes]
|
|
||||||
,s.[DateModified]
|
|
||||||
,s.[DateOfCreation]
|
|
||||||
,[DueDateRangeStart]
|
|
||||||
,[DueDateRangeEnd]
|
|
||||||
,[PortalLink]
|
|
||||||
,[CostCenterId]
|
|
||||||
,[Description]
|
|
||||||
,[State]
|
|
||||||
,s.[CustomValues]
|
|
||||||
,[CurrentOwnerId]
|
|
||||||
,s.[CustomerId]
|
|
||||||
,[ParentServiceRequestId]
|
|
||||||
,[ParentExtenalId] = cast(CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then null ELSE '?' END as nvarchar(255))
|
|
||||||
,[ParentWorkDoneAt] = cast(NULL as datetime)
|
|
||||||
,[ParentInvoiceId] = cast(NULL as nvarchar(255))
|
|
||||||
,[Location#ID]
|
|
||||||
,s.[Version]
|
|
||||||
,[IsTemplate]
|
|
||||||
,[IsTemplateMobile]
|
|
||||||
,[CreateFromServiceRequestTemplateId]
|
|
||||||
,[Type]
|
|
||||||
,[CustomerName] = NULL
|
|
||||||
,[isChild] = CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then 0 ELSE 1 END
|
|
||||||
,[open] = CASE WHEN s.[workdoneat] is not null AND [dbo].[fds__fn_InvoiceIDs_mfr](s.[invoiceid],'') = '' AND ISNULL(inv.[isOpen],1) = 1 THEN 1 ELSE 0 END
|
|
||||||
,[baseorder] = 0
|
|
||||||
FROM [dbo].[mfr__servicerequests] as s -- parent and childs;
|
|
||||||
LEFT JOIN @inv as inv on inv.[Id] = s.[Id]
|
|
||||||
WHERE
|
|
||||||
([type] in ('IsProject', 'IsMaintenance')
|
|
||||||
AND
|
|
||||||
(CASE WHEN @openmode = 1 AND @mode = 'r' THEN
|
|
||||||
( CASE WHEN
|
|
||||||
[workdoneat] is not null
|
|
||||||
--AND ISNULL(s.[invoiceid],'') = ''
|
|
||||||
AND ISNULL([State], '') <> 'Closed'
|
|
||||||
--AND ISNULL(inv.[isOpen],1) = 1
|
|
||||||
AND NOT EXISTS (SELECT * FROM [dbo].[fds__custom_servicerequest] as sr where sr.[EntityID] = s.[id] and sr.[hidden] = 1)
|
|
||||||
AND (CAST([DateOfCreation] as date) BETWEEN @startdate AND @today OR CAST([DateModified] as date) BETWEEN @startdate AND @today)
|
|
||||||
AND EXISTS (SELECT 0 FROM [dbo].[mfr__*PartnerSet] as ps JOIN [dbo].[mfr__tags] as tg on ps.Property = 'ServiceRequest:Tags' and tg.Id = ps.[PartnerId] where EntityId = s.[id] and ps.partnerid = 16034758659 and [Type] = 'ServiceRequest')
|
|
||||||
THEN 1 ELSE 0 END)
|
|
||||||
WHEN @openmode = 1 AND @mode <> 'r' THEN
|
|
||||||
( CASE WHEN
|
|
||||||
[workdoneat] is not null
|
|
||||||
AND ISNULL(s.[invoiceid],'') = ''
|
|
||||||
AND ISNULL([State], '') <> 'Closed'
|
|
||||||
AND ISNULL(inv.[isOpen],1) = 1
|
|
||||||
AND NOT EXISTS (SELECT * FROM [dbo].[fds__custom_servicerequest] as sr where sr.[EntityID] = s.[id] and sr.[hidden] = 1)
|
|
||||||
THEN 1 ELSE 0 END)
|
|
||||||
WHEN @openmode = 0 THEN
|
|
||||||
( CASE WHEN CAST([DateOfCreation] as date) BETWEEN @startdate AND @enddate THEN 1 ELSE 0 END)
|
|
||||||
ELSE 0 END
|
|
||||||
) = 1);
|
|
||||||
|
|
||||||
INSERT INTO @SReq
|
|
||||||
SELECT DISTINCT
|
|
||||||
ISNULL(s.[Id], CASE WHEN ISNULL(q.[ParentServiceRequestId],0) > 0 THEN q.[ParentServiceRequestId] ELSE NULL END)
|
|
||||||
,ISNULL(s.[Name], CASE WHEN ISNULL(q.[ParentServiceRequestId],0) > 0 THEN ' - - unbekannt - - ' ELSE NULL END)
|
|
||||||
,s.[ExternalId]
|
|
||||||
,[InvoiceId] = [dbo].[fds__fn_InvoiceIDs_mfr](s.[invoiceid], inv.[Invoices])
|
|
||||||
,s.[ClosedAt]
|
|
||||||
,s.[ReleasedAt]
|
|
||||||
,s.[WorkDoneAt]
|
|
||||||
,s.[TargetTimeInMinutes]
|
|
||||||
,s.[DateModified]
|
|
||||||
,s.[DateOfCreation]
|
|
||||||
,s.[DueDateRangeStart]
|
|
||||||
,s.[DueDateRangeEnd]
|
|
||||||
,s.[PortalLink]
|
|
||||||
,s.[CostCenterId]
|
|
||||||
,s.[Description]
|
|
||||||
,s.[State]
|
|
||||||
,s.[CustomValues]
|
|
||||||
,s.[CurrentOwnerId]
|
|
||||||
,s.[CustomerId]
|
|
||||||
,s.[ParentServiceRequestId]
|
|
||||||
,[ParentExtenalId] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[ExternalId] ELSE NULL END
|
|
||||||
,[ParentWorkDoneAt] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[WorkDoneAt] ELSE NULL END
|
|
||||||
,[ParentInvoiceId] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[InvoiceId] ELSE NULL END
|
|
||||||
,s.[Location#ID]
|
|
||||||
,s.[Version]
|
|
||||||
,s.[IsTemplate]
|
|
||||||
,s.[IsTemplateMobile]
|
|
||||||
,s.[CreateFromServiceRequestTemplateId]
|
|
||||||
,s.[Type]
|
|
||||||
,[CustomerName] = NULL
|
|
||||||
,[isChild] = CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then 0 ELSE 1 END
|
|
||||||
,[open] = CASE WHEN s.[workdoneat] is not null AND [dbo].[fds__fn_InvoiceIDs_mfr](s.[invoiceid],'') = '' AND ISNULL(inv.[isOpen],1) = 1 THEN 1 ELSE 0 END
|
|
||||||
,[baseorder] = 0
|
|
||||||
from [dbo].[mfr__servicerequests] as s RIGHT JOIN @SReq as q ON (s.[ParentServiceRequestId] = q.[id] OR q.[ParentServiceRequestId] = s.[id]) --parents or childs
|
|
||||||
LEFT JOIN @inv as inv on inv.[Id] = s.[Id]
|
|
||||||
WHERE
|
|
||||||
(s.[type] in ('IsProject', 'IsMaintenance')
|
|
||||||
AND NOT EXISTS (SELECT * FROM @SReq as s2 WHERE s2.[id] = s.[Id])
|
|
||||||
AND ISNULL(s.[Id], CASE WHEN ISNULL(q.[ParentServiceRequestId],0) > 0 THEN q.[ParentServiceRequestId] ELSE NULL END) is not null
|
|
||||||
)
|
|
||||||
;
|
|
||||||
|
|
||||||
|
|
||||||
WITH q as (
|
|
||||||
SELECT [id], [baseorder] = (ROW_NUMBER() OVER (ORDER BY ISNULL([ParentWorkDoneAt], [WorkDoneAt]), CASE WHEN ISNULL([ParentServiceRequestId],0) = 0 THEN [id] ELSE [ParentServiceRequestId] END) * 100)
|
|
||||||
FROM @SReq as s
|
|
||||||
WHERE [ischild] = 0
|
|
||||||
)
|
|
||||||
UPDATE s SET
|
|
||||||
[baseorder] = q.baseorder
|
|
||||||
,[ParentServiceRequestId] = CASE WHEN s.isChild = 0 THEN s.[Id] ELSE s.[ParentServiceRequestId] END
|
|
||||||
FROM @SReq as s JOIN q ON s.[id] = q.[id];
|
|
||||||
|
|
||||||
|
|
||||||
UPDATE s SET
|
|
||||||
[CustomerName] = c.[name]
|
|
||||||
FROM @SReq as s JOIN [mfr__companies] as c ON s.[CustomerId] = c.[id];
|
|
||||||
|
|
||||||
|
|
||||||
WITH q as (
|
|
||||||
SELECT s.[id]
|
|
||||||
, q.[ExternalId], q.[WorkDoneAt], q.[InvoiceId]
|
|
||||||
, [baseorder] = q.[baseorder] + ROW_NUMBER() OVER (PARTITION BY q.[id] ORDER BY s.[dateofcreation], s.[Id])
|
|
||||||
FROM @SReq as s JOIN @SReq as q ON s.[ParentServiceRequestId] = q.[id]
|
|
||||||
WHERE s.[ischild] = 1
|
|
||||||
)
|
|
||||||
UPDATE s SET
|
|
||||||
[ParentExtenalId] = q.[ExternalId]
|
|
||||||
,[ParentWorkDoneAt] = q.[WorkDoneAt]
|
|
||||||
,[ParentInvoiceId] = q.[InvoiceId]
|
|
||||||
,[baseorder] = q.[baseorder]
|
|
||||||
FROM @SReq as s JOIN q ON s.[Id] = q.[id];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
---OUTPUT
|
|
||||||
|
|
||||||
WITH tgs as(
|
|
||||||
SELECT ps.[EntityId], tg.* FROM [dbo].[mfr__tags] as tg JOIN [dbo].[mfr__*PartnerSet] as ps ON ps.[Property] = 'ServiceRequest:Tags' and tg.[Id] = ps.[PartnerId] AND ISNULL(tg.[Name],'') <> '' and [Type] = 'ServiceRequest'
|
|
||||||
WHERE tg.Id in (16288120843
|
|
||||||
,15842181123
|
|
||||||
,16034758659
|
|
||||||
,16050520064
|
|
||||||
,16783966210
|
|
||||||
,16802480128
|
|
||||||
,16830398466
|
|
||||||
,19650019330
|
|
||||||
,19670532096
|
|
||||||
,18925977600)
|
|
||||||
AND EXISTS (SELECT 0 FROM @SReq as _s where _s.Id = ps.[EntityId])
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
s.*
|
|
||||||
,[tags] = STUFF( (SELECT ',' + TRIM([Name]) FROM tgs where tgs.[EntityId] = s.[Id] order by tgs.[name] FOR XML PATH, TYPE).value('.[1]','varchar(255)'), 1,1,'')
|
|
||||||
FROM @SReq as s order by baseorder
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
[requestID] = s.[Id]
|
|
||||||
,r.*
|
|
||||||
,[order] = ROW_NUMBER() OVER (PARTITION BY s.[Id] ORDER BY r.[dateofcreation] ASC, r.[Id])
|
|
||||||
FROM [dbo].[mfr__invoices] as r
|
|
||||||
JOIN [dbo].[mfr__*PartnerSet] as ps ON ps.[Property] like 'ServiceRequest:Invoices' and r.[id] = ps.PartnerId
|
|
||||||
JOIN @SReq as s ON s.[Id] = ps.[EntityId]
|
|
||||||
ORDER BY s.[Id], r.[dateofcreation] ASC;
|
|
||||||
|
|
||||||
END
|
|
||||||
@@ -1,308 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__getRequests_list2]
|
|
||||||
@tgtdate date
|
|
||||||
,@mode varchar(1) = ''
|
|
||||||
,@completed bit = 1
|
|
||||||
, @search varchar(100)
|
|
||||||
,@authuser varchar(100)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
IF [dbo].[fis_getModuleAuth]('fds_req', @authuser) < 1
|
|
||||||
THROW 60000, N'not authorized', 1;
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info])
|
|
||||||
VALUES ('fds__getRequests_list' ,@authuser , @search);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @startdate date, @enddate date, @today date = GETDATE();
|
|
||||||
DECLARE @searchmode as varchar(1) = CASE WHEN SUBSTRING(ISNULL(@search, '') + ' ', 2,1) <> ':' THEN '' ELSE LOWER(LEFT(@search,1 )) END;
|
|
||||||
|
|
||||||
IF @searchmode <> ''
|
|
||||||
BEGIN
|
|
||||||
SELECT @mode = 's', @search = SUBSTRING(@search,3, 1000);
|
|
||||||
END
|
|
||||||
ELSE
|
|
||||||
SET @mode = LOWER(@mode);
|
|
||||||
|
|
||||||
If @mode in ('r') -- r = open recent
|
|
||||||
BEGIN
|
|
||||||
SET @tgtdate = @today;
|
|
||||||
SELECT @startdate = DATEADD(week, -4, [dbo].[date_weekfirst](@tgtdate))
|
|
||||||
,@enddate = @today;
|
|
||||||
END
|
|
||||||
else If @mode in ('m') -- r = open recent
|
|
||||||
BEGIN
|
|
||||||
SELECT @startdate = [dbo].[date_monthfirst](@tgtdate)
|
|
||||||
,@enddate = [dbo].[date_monthend](@tgtdate);
|
|
||||||
END
|
|
||||||
ELSE If @mode = 'w'
|
|
||||||
BEGIN
|
|
||||||
SELECT @startdate = [dbo].[date_weekfirst](@tgtdate)
|
|
||||||
,@enddate = [dbo].[date_weekend](@tgtdate);
|
|
||||||
END
|
|
||||||
|
|
||||||
DECLARE @openmode as bit = CASE WHEN LOWER(LEFT(ISNULL(@mode, ''),1)) IN ('o', 'r') THEN 1 ELSE 0 END;
|
|
||||||
SELECT [startdate] = @startdate, [enddate] = @enddate, [mode] = @mode
|
|
||||||
, [title] = CASE WHEN @searchmode = 'n' THEN 'Übersicht der Aufträge mit der Auftragsnummer ' + @search
|
|
||||||
WHEN @openmode = 1 AND @mode = 'r' THEN 'Übersicht der offenen Aufträge (letzte 4 Wochen seit ' + FORMAT(@startdate, 'dd.MM.yyyy') + ')'
|
|
||||||
WHEN @openmode = 1 THEN 'Übersicht der offenen Aufträge'
|
|
||||||
ELSE 'Auftragsübersicht für ' + CASE WHEN @mode = 'm' THEN 'MONAT ' + FORMAT(@startdate, 'MMM yyyy')
|
|
||||||
WHEN @mode = 'w' THEN 'WOCHE ' + FORMAT(@startdate, 'dd.MM.yyyy') + ' - ' + FORMAT(@enddate, 'dd.MM.')
|
|
||||||
ELSE '' END
|
|
||||||
END
|
|
||||||
, [note] = CASE WHEN @searchmode = 'n' THEN 'Es wird nur der Auftrag mit der Nummer ' + @search + ' angezeigt'
|
|
||||||
WHEN @openmode = 1 and @mode = 'r' THEN 'Es werden alle Aufträge angezeigt, die selbst offen sind und seit ' + FORMAT(@startdate, 'dd.MM.yyyy') + ' erstellt wurden, oder deren Folgeauftrag offen ist und/oder seit ' + FORMAT(@startdate, 'dd.MM.yyyy') + ' erstellt wurden.'
|
|
||||||
WHEN @openmode = 1 THEN 'Es werden alle Aufträge angezeigt, die selbst oder ein Folgeeauftrag offen ist.'
|
|
||||||
ELSE '' END;
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @SReq TABLE (
|
|
||||||
[Id] [bigint] NOT NULL,
|
|
||||||
[Name] [nvarchar](255) NULL,
|
|
||||||
[ExternalId] [nvarchar](255) NULL,
|
|
||||||
[InvoiceId] varchar(1000) NULL,
|
|
||||||
[ClosedAt] [datetime] NULL,
|
|
||||||
[ReleasedAt] [datetime] NULL,
|
|
||||||
[WorkDoneAt] [datetime] NULL,
|
|
||||||
[TargetTimeInMinutes] [bigint] NULL,
|
|
||||||
[DateModified] [datetime] NULL,
|
|
||||||
[DateOfCreation] [datetime] NULL,
|
|
||||||
[DueDateRangeStart] [datetime] NULL,
|
|
||||||
[DueDateRangeEnd] [datetime] NULL,
|
|
||||||
[PortalLink] [nvarchar](255) NULL,
|
|
||||||
[CostCenterId] [bigint] NULL,
|
|
||||||
[Description] [nvarchar](max) NULL,
|
|
||||||
[State] [nvarchar](255) NULL,
|
|
||||||
[CustomValues] [nvarchar](max) NULL,
|
|
||||||
[CurrentOwnerId] [bigint] NULL,
|
|
||||||
[CustomerId] [bigint] NULL,
|
|
||||||
[ParentServiceRequestId] [bigint] NULL,
|
|
||||||
[ParentExtenalId] [nvarchar](255) NULL,
|
|
||||||
[ParentWorkDoneAt] [date] NULL,
|
|
||||||
[ParentInvoiceId] varchar(1000) NULL,
|
|
||||||
[Location#ID] [bigint] NULL,
|
|
||||||
[Version] [int] NULL,
|
|
||||||
[IsTemplate] [bit] NULL,
|
|
||||||
[IsTemplateMobile] [bit] NULL,
|
|
||||||
[CreateFromServiceRequestTemplateId] [bigint] NULL,
|
|
||||||
[Type] [nvarchar](255) NULL,
|
|
||||||
[CustomerName] [nvarchar](255) NULL,
|
|
||||||
[isChild] bit NULL,
|
|
||||||
[open] bit,
|
|
||||||
[baseorder] int
|
|
||||||
);
|
|
||||||
DECLARE @inv TABLE ([Id] bigint, [Invoices] varchar(1000), [isOpen] bit);
|
|
||||||
With fdsinv as(
|
|
||||||
SELECT [Id] = fdsr.[mfr__servicerequest], fdsi.[InvoiceId], fdsi.[InvoiceType], [isCanceled] = ISNULL(fdsi.[isCanceled],0) FROM [dbo].[fds__invoice_servicerequests] as fdsr
|
|
||||||
JOIN [dbo].[fds__invoices] as fdsi on fdsr.[invid] = fdsi.[id]
|
|
||||||
WHERE [isFinal] = 1 AND fdsr.[mfr__servicerequest] is not null
|
|
||||||
), fi as(
|
|
||||||
SELECT [Id], [isOpen] = MIN(CASE WHEN ISNULL([invoicetype],'r') = 'r' AND [isCanceled] = 0 THEN 0 ELSE 1 END) FROM fdsinv GROUP BY [Id]
|
|
||||||
)
|
|
||||||
INSERT INTO @inv
|
|
||||||
SELECT [Id]
|
|
||||||
, [invoices] = ISNULL(STUFF( (SELECT CHAR(10) + [InvoiceId] + ' (' + [InvoiceTYpe] + ')' FROM fdsinv as f2 WHERE f2.[id] = fi.[Id] FOR XML PATH, TYPE).value('.[1]','nvarchar(max)') ,1,1,'') , '')
|
|
||||||
, [isOpen]
|
|
||||||
FROM fi;
|
|
||||||
|
|
||||||
WITH s as (
|
|
||||||
SELECT * FROM [dbo].[mfr__servicerequests] as _s
|
|
||||||
WHERE (CASE
|
|
||||||
WHEN @searchmode = 'n' THEN
|
|
||||||
( CASE WHEN [ExternalId] = @search OR [ExternalId] like ('_[-]' + @search) THEN 1 ELSE 0 END )
|
|
||||||
WHEN @openmode = 1 AND @mode = 'r' THEN
|
|
||||||
( CASE WHEN
|
|
||||||
[workdoneat] is not null
|
|
||||||
--AND ISNULL(s.[invoiceid],'') = ''
|
|
||||||
AND ISNULL([State], '') <> 'Closed'
|
|
||||||
--AND ISNULL(inv.[isOpen],1) = 1
|
|
||||||
AND NOT EXISTS (SELECT * FROM [dbo].[fds__custom_servicerequest] as sr where sr.[EntityID] = _s.[id] and sr.[hidden] = 1)
|
|
||||||
AND (CAST([DateOfCreation] as date) BETWEEN @startdate AND @today OR CAST([DateModified] as date) BETWEEN @startdate AND @today)
|
|
||||||
AND EXISTS (SELECT 0 FROM [dbo].[mfr__*PartnerSet] as ps JOIN [dbo].[mfr__tags] as tg on ps.Property = 'ServiceRequest:Tags' and tg.Id = ps.[PartnerId] where EntityId = _s.[id] and ps.partnerid = 16034758659 and [Type] = 'ServiceRequest')
|
|
||||||
THEN 1 ELSE 0 END)
|
|
||||||
WHEN @openmode = 1 AND @mode <> 'r' THEN -- will be covered later because invoice info is included
|
|
||||||
1
|
|
||||||
--( CASE WHEN
|
|
||||||
-- [workdoneat] is not null
|
|
||||||
-- AND ISNULL(s.[invoiceid],'') = ''
|
|
||||||
-- AND ISNULL([State], '') <> 'Closed'
|
|
||||||
-- AND ISNULL(inv.[isOpen],1) = 1
|
|
||||||
-- AND NOT EXISTS (SELECT * FROM [dbo].[fds__custom_servicerequest] as sr where sr.[EntityID] = s.[id] and sr.[hidden] = 1)
|
|
||||||
-- THEN 1 ELSE 0 END)
|
|
||||||
WHEN @openmode = 0 THEN
|
|
||||||
( CASE WHEN CAST([DateOfCreation] as date) BETWEEN @startdate AND @enddate THEN 1 ELSE 0 END)
|
|
||||||
ELSE 0 END
|
|
||||||
) = 1
|
|
||||||
)
|
|
||||||
insert into @SReq
|
|
||||||
SELECT
|
|
||||||
s.[Id]
|
|
||||||
,[Name]
|
|
||||||
,[ExternalId]
|
|
||||||
,[InvoiceId] = [dbo].[fds__fn_InvoiceIDs_mfr](s.[invoiceid], inv.[Invoices])
|
|
||||||
,[ClosedAt]
|
|
||||||
,[ReleasedAt]
|
|
||||||
,[WorkDoneAt]
|
|
||||||
,[TargetTimeInMinutes]
|
|
||||||
,s.[DateModified]
|
|
||||||
,s.[DateOfCreation]
|
|
||||||
,[DueDateRangeStart]
|
|
||||||
,[DueDateRangeEnd]
|
|
||||||
,[PortalLink]
|
|
||||||
,[CostCenterId]
|
|
||||||
,[Description]
|
|
||||||
,[State]
|
|
||||||
,s.[CustomValues]
|
|
||||||
,[CurrentOwnerId]
|
|
||||||
,s.[CustomerId]
|
|
||||||
,[ParentServiceRequestId]
|
|
||||||
,[ParentExtenalId] = cast(CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then null ELSE '?' END as nvarchar(255))
|
|
||||||
,[ParentWorkDoneAt] = cast(NULL as datetime)
|
|
||||||
,[ParentInvoiceId] = cast(NULL as nvarchar(255))
|
|
||||||
,[Location#ID]
|
|
||||||
,s.[Version]
|
|
||||||
,[IsTemplate]
|
|
||||||
,[IsTemplateMobile]
|
|
||||||
,[CreateFromServiceRequestTemplateId]
|
|
||||||
,[Type]
|
|
||||||
,[CustomerName] = NULL
|
|
||||||
,[isChild] = CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then 0 ELSE 1 END
|
|
||||||
,[open] = CASE WHEN s.[workdoneat] is not null AND [dbo].[fds__fn_InvoiceIDs_mfr](s.[invoiceid],'') = '' AND ISNULL(inv.[isOpen],1) = 1 THEN 1 ELSE 0 END
|
|
||||||
,[baseorder] = 0
|
|
||||||
FROM s -- parent and childs;
|
|
||||||
LEFT JOIN @inv as inv on inv.[Id] = s.[Id]
|
|
||||||
WHERE
|
|
||||||
([type] in ('IsProject', 'IsMaintenance')
|
|
||||||
AND
|
|
||||||
(CASE
|
|
||||||
WHEN @openmode = 1 AND @mode <> 'r' THEN
|
|
||||||
( CASE WHEN
|
|
||||||
[workdoneat] is not null
|
|
||||||
AND ISNULL(s.[invoiceid],'') = ''
|
|
||||||
AND ISNULL([State], '') <> 'Closed'
|
|
||||||
AND ISNULL(inv.[isOpen],1) = 1
|
|
||||||
AND NOT EXISTS (SELECT * FROM [dbo].[fds__custom_servicerequest] as sr where sr.[EntityID] = s.[id] and sr.[hidden] = 1)
|
|
||||||
THEN 1 ELSE 0 END)
|
|
||||||
ELSE 1 END -- heres the difference, because main filter is applied in CTE
|
|
||||||
) = 1);
|
|
||||||
|
|
||||||
|
|
||||||
INSERT INTO @SReq
|
|
||||||
SELECT DISTINCT
|
|
||||||
ISNULL(s.[Id], CASE WHEN ISNULL(q.[ParentServiceRequestId],0) > 0 THEN q.[ParentServiceRequestId] ELSE NULL END)
|
|
||||||
,ISNULL(s.[Name], CASE WHEN ISNULL(q.[ParentServiceRequestId],0) > 0 THEN ' - - unbekannt - - ' ELSE NULL END)
|
|
||||||
,s.[ExternalId]
|
|
||||||
,[InvoiceId] = [dbo].[fds__fn_InvoiceIDs_mfr](s.[invoiceid], inv.[Invoices])
|
|
||||||
,s.[ClosedAt]
|
|
||||||
,s.[ReleasedAt]
|
|
||||||
,s.[WorkDoneAt]
|
|
||||||
,s.[TargetTimeInMinutes]
|
|
||||||
,s.[DateModified]
|
|
||||||
,s.[DateOfCreation]
|
|
||||||
,s.[DueDateRangeStart]
|
|
||||||
,s.[DueDateRangeEnd]
|
|
||||||
,s.[PortalLink]
|
|
||||||
,s.[CostCenterId]
|
|
||||||
,s.[Description]
|
|
||||||
,s.[State]
|
|
||||||
,s.[CustomValues]
|
|
||||||
,s.[CurrentOwnerId]
|
|
||||||
,s.[CustomerId]
|
|
||||||
,s.[ParentServiceRequestId]
|
|
||||||
,[ParentExtenalId] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[ExternalId] ELSE NULL END
|
|
||||||
,[ParentWorkDoneAt] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[WorkDoneAt] ELSE NULL END
|
|
||||||
,[ParentInvoiceId] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[InvoiceId] ELSE NULL END
|
|
||||||
,s.[Location#ID]
|
|
||||||
,s.[Version]
|
|
||||||
,s.[IsTemplate]
|
|
||||||
,s.[IsTemplateMobile]
|
|
||||||
,s.[CreateFromServiceRequestTemplateId]
|
|
||||||
,s.[Type]
|
|
||||||
,[CustomerName] = NULL
|
|
||||||
,[isChild] = CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then 0 ELSE 1 END
|
|
||||||
,[open] = CASE WHEN s.[workdoneat] is not null AND [dbo].[fds__fn_InvoiceIDs_mfr](s.[invoiceid],'') = '' AND ISNULL(inv.[isOpen],1) = 1 THEN 1 ELSE 0 END
|
|
||||||
,[baseorder] = 0
|
|
||||||
from [dbo].[mfr__servicerequests] as s RIGHT JOIN @SReq as q ON (s.[ParentServiceRequestId] = q.[id] OR q.[ParentServiceRequestId] = s.[id]) --parents or childs
|
|
||||||
LEFT JOIN @inv as inv on inv.[Id] = s.[Id]
|
|
||||||
WHERE
|
|
||||||
(s.[type] in ('IsProject', 'IsMaintenance')
|
|
||||||
AND NOT EXISTS (SELECT * FROM @SReq as s2 WHERE s2.[id] = s.[Id])
|
|
||||||
AND ISNULL(s.[Id], CASE WHEN ISNULL(q.[ParentServiceRequestId],0) > 0 THEN q.[ParentServiceRequestId] ELSE NULL END) is not null
|
|
||||||
)
|
|
||||||
;
|
|
||||||
|
|
||||||
|
|
||||||
WITH q as (
|
|
||||||
SELECT [id], [baseorder] = (ROW_NUMBER() OVER (ORDER BY ISNULL([ParentWorkDoneAt], [WorkDoneAt]), CASE WHEN ISNULL([ParentServiceRequestId],0) = 0 THEN [id] ELSE [ParentServiceRequestId] END) * 100)
|
|
||||||
FROM @SReq as s
|
|
||||||
WHERE [ischild] = 0
|
|
||||||
)
|
|
||||||
UPDATE s SET
|
|
||||||
[baseorder] = q.baseorder
|
|
||||||
,[ParentServiceRequestId] = CASE WHEN s.isChild = 0 THEN s.[Id] ELSE s.[ParentServiceRequestId] END
|
|
||||||
FROM @SReq as s JOIN q ON s.[id] = q.[id];
|
|
||||||
|
|
||||||
|
|
||||||
UPDATE s SET
|
|
||||||
[CustomerName] = c.[name]
|
|
||||||
FROM @SReq as s JOIN [mfr__companies] as c ON s.[CustomerId] = c.[id];
|
|
||||||
|
|
||||||
|
|
||||||
WITH q as (
|
|
||||||
SELECT s.[id]
|
|
||||||
, q.[ExternalId], q.[WorkDoneAt], q.[InvoiceId]
|
|
||||||
, [baseorder] = q.[baseorder] + ROW_NUMBER() OVER (PARTITION BY q.[id] ORDER BY s.[dateofcreation], s.[Id])
|
|
||||||
FROM @SReq as s JOIN @SReq as q ON s.[ParentServiceRequestId] = q.[id]
|
|
||||||
WHERE s.[ischild] = 1
|
|
||||||
)
|
|
||||||
UPDATE s SET
|
|
||||||
[ParentExtenalId] = q.[ExternalId]
|
|
||||||
,[ParentWorkDoneAt] = q.[WorkDoneAt]
|
|
||||||
,[ParentInvoiceId] = q.[InvoiceId]
|
|
||||||
,[baseorder] = q.[baseorder]
|
|
||||||
FROM @SReq as s JOIN q ON s.[Id] = q.[id];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
---OUTPUT
|
|
||||||
|
|
||||||
WITH tgs as(
|
|
||||||
SELECT ps.[EntityId], tg.* FROM [dbo].[mfr__tags] as tg JOIN [dbo].[mfr__*PartnerSet] as ps ON ps.[Property] = 'ServiceRequest:Tags' and tg.[Id] = ps.[PartnerId] AND ISNULL(tg.[Name],'') <> '' and [Type] = 'ServiceRequest'
|
|
||||||
WHERE tg.Id in (16288120843
|
|
||||||
,15842181123
|
|
||||||
,16034758659
|
|
||||||
,16050520064
|
|
||||||
,16783966210
|
|
||||||
,16802480128
|
|
||||||
,16830398466
|
|
||||||
,19650019330
|
|
||||||
,19670532096
|
|
||||||
,18925977600)
|
|
||||||
AND EXISTS (SELECT 0 FROM @SReq as _s where _s.Id = ps.[EntityId])
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
s.*
|
|
||||||
,[tags] = STUFF( (SELECT ',' + TRIM([Name]) FROM tgs where tgs.[EntityId] = s.[Id] order by tgs.[name] FOR XML PATH, TYPE).value('.[1]','varchar(255)'), 1,1,'')
|
|
||||||
FROM @SReq as s order by baseorder
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
[requestID] = s.[Id]
|
|
||||||
,r.*
|
|
||||||
,[order] = ROW_NUMBER() OVER (PARTITION BY s.[Id] ORDER BY r.[dateofcreation] ASC, r.[Id])
|
|
||||||
FROM [dbo].[mfr__invoices] as r
|
|
||||||
JOIN [dbo].[mfr__*PartnerSet] as ps ON ps.[Property] like 'ServiceRequest:Invoices' and r.[id] = ps.PartnerId
|
|
||||||
JOIN @SReq as s ON s.[Id] = ps.[EntityId]
|
|
||||||
ORDER BY s.[Id], r.[dateofcreation] ASC;
|
|
||||||
|
|
||||||
END
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__lookupReminders]
|
|
||||||
@InvId varchar(15)
|
|
||||||
, @authuser varchar(100)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
IF [dbo].[fis_getModuleAuth]('fds_reminder', @authuser) < 2
|
|
||||||
THROW 60000, N'not authorized', 1;
|
|
||||||
ELSE IF NOT EXISTS (select 0 FROM [dbo].[fds__invoices] WHERE [id] = @InvId) AND NOT EXISTS (select 0 FROM [dbo].[mfr__invoices] WHERE [id] = TRY_CAST(@InvId as bigint))
|
|
||||||
THROW 60000, N'invoice not found', 1;
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info])
|
|
||||||
VALUES ('fds__lookupReminders' ,@authuser , (SELECT * FROM (VALUES(@InvId, @authuser)) as z ([InvId],[authuser]) FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER));
|
|
||||||
|
|
||||||
DECLARE @REM TABLE (
|
|
||||||
[Id] [varchar](10) NOT NULL,
|
|
||||||
[Version] [int] NOT NULL,
|
|
||||||
[DocumentName] [varchar](100) NULL,
|
|
||||||
[InvId] [varchar](15) NOT NULL,
|
|
||||||
[CustomerId] [bigint] NULL,
|
|
||||||
[SendToAddress] [nvarchar](1000) NULL,
|
|
||||||
[SendToEmail] [nvarchar](255) NULL,
|
|
||||||
[type] [varchar](3) NOT NULL,
|
|
||||||
[amount] [numeric](10, 3) NULL,
|
|
||||||
[amount_payed] [numeric](10, 3) NULL,
|
|
||||||
[amount_open] [numeric](10, 3) NULL,
|
|
||||||
[subject] [nvarchar](255) NULL,
|
|
||||||
[text] [nvarchar](2000) NULL,
|
|
||||||
[IsSent] [bit] NOT NULL,
|
|
||||||
[IsFinal] [bit] NULL,
|
|
||||||
[CustomValues] [nvarchar](max) NULL,
|
|
||||||
[DateSent] [datetime] NULL,
|
|
||||||
[UserSent] [varchar](25) NULL,
|
|
||||||
[DateFinalized] [datetime] NULL,
|
|
||||||
[UserFinalized] [varchar](25) NULL,
|
|
||||||
[DateCreated] [datetime] NOT NULL,
|
|
||||||
[UserCreated] [varchar](25) NOT NULL,
|
|
||||||
[DateModified] [datetime] NOT NULL,
|
|
||||||
[UserModified] [varchar](25) NOT NULL,
|
|
||||||
[order] int
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO @REM
|
|
||||||
([Id]
|
|
||||||
,[Version]
|
|
||||||
,[DocumentName]
|
|
||||||
,[InvId]
|
|
||||||
,[CustomerId]
|
|
||||||
,[SendToAddress]
|
|
||||||
,[SendToEmail]
|
|
||||||
,[type]
|
|
||||||
,[amount]
|
|
||||||
,[amount_payed]
|
|
||||||
,[subject]
|
|
||||||
,[text]
|
|
||||||
,[IsSent]
|
|
||||||
,[IsFinal]
|
|
||||||
,[CustomValues]
|
|
||||||
,[DateSent]
|
|
||||||
,[UserSent]
|
|
||||||
,[DateFinalized]
|
|
||||||
,[UserFinalized]
|
|
||||||
,[DateCreated]
|
|
||||||
,[UserCreated]
|
|
||||||
,[DateModified]
|
|
||||||
,[UserModified]
|
|
||||||
,[order])
|
|
||||||
SELECT [Id]
|
|
||||||
,[Version]
|
|
||||||
,[DocumentName]
|
|
||||||
,[InvId]
|
|
||||||
,[CustomerId]
|
|
||||||
,[SendToAddress]
|
|
||||||
,[SendToEmail]
|
|
||||||
,[type]
|
|
||||||
,[amount]
|
|
||||||
,[amount_payed]
|
|
||||||
,[subject]
|
|
||||||
,[text]
|
|
||||||
,[IsSent]
|
|
||||||
,[IsFinal]
|
|
||||||
,[CustomValues]
|
|
||||||
,[DateSent]
|
|
||||||
,[UserSent]
|
|
||||||
,[DateFinalized]
|
|
||||||
,[UserFinalized]
|
|
||||||
,[DateCreated]
|
|
||||||
,[UserCreated]
|
|
||||||
,[DateModified]
|
|
||||||
,[UserModified]
|
|
||||||
, [order] = ROW_NUMBER() OVER (ORDER BY DateFinalized ASC)
|
|
||||||
FROM [dbo].[fds__reminder] WHERE [invid] = @InvId AND [IsFinal] = 1;
|
|
||||||
|
|
||||||
DECLARE @last varchar(1) = ISNULL(( SELECT TOP(1) [type] FROM @rem ORDER BY [DateFinalized] DESC), '');
|
|
||||||
DECLARE @newnum int = CASE WHEN @last IN('l','f') THEN (SELECT COUNT(*) FROM @REM) + 1
|
|
||||||
WHEN @last = 'm' THEN ISNULL( (SELECT TOP(1) TRY_CAST(LEFT([type],1) as int) FROM @rem ORDER BY [DateFinalized] DESC) , (SELECT COUNT(*) FROM @REM) + 1)
|
|
||||||
ELSE 1
|
|
||||||
END;
|
|
||||||
|
|
||||||
---output
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
[type] = CAST( CASE WHEN @last = 'f' THEN 'm' WHEN @last = 'm' THEN 'l' ELSE 'f' END as varchar(1))
|
|
||||||
,[level] = @newnum;
|
|
||||||
|
|
||||||
SELECT [subject], [amount], [amount_payed], [DateFinalized] FROM @REM;
|
|
||||||
|
|
||||||
END
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__prepReminder]
|
|
||||||
@InvId varchar(15)
|
|
||||||
, @authuser varchar(100)
|
|
||||||
, @type varchar(1)
|
|
||||||
, @level int
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
IF [dbo].[fis_getModuleAuth]('fds_reminder', @authuser) < 2
|
|
||||||
THROW 60000, N'not authorized', 1;
|
|
||||||
ELSE IF ISNULL(@type,'') NOT IN ('f','m','l') OR ISNULL(@level, 0) < 1
|
|
||||||
THROW 60000, N'inputs not valid', 1;
|
|
||||||
ELSE IF NOT EXISTS (select 0 FROM [dbo].[fds__invoices] WHERE [id] = @InvId) AND NOT EXISTS (select 0 FROM [dbo].[mfr__invoices] WHERE [id] = TRY_CAST(@InvId as bigint))
|
|
||||||
THROW 60000, N'invoice not found', 1;
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info])
|
|
||||||
VALUES ('fds__prepReminder' ,@authuser , (SELECT * FROM (VALUES(@InvId, @authuser, @type, @level)) as z ([InvId],[authuser],[type],[level]) FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER));
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @now datetime = GETDATE();
|
|
||||||
DECLARE @today date = CAST(@now as date);
|
|
||||||
|
|
||||||
|
|
||||||
--output
|
|
||||||
|
|
||||||
With inv1 as(
|
|
||||||
select TOP(1) * FROM [dbo].[fds__invoices] WHERE [id] = @InvId
|
|
||||||
), inv as(
|
|
||||||
SELECT [Id] = cast([id] as varchar(15))
|
|
||||||
,[PaymentTerm]
|
|
||||||
,[SendToemail]
|
|
||||||
,[SendToAddress]
|
|
||||||
,[CustomerId]
|
|
||||||
,[Invoiceid]
|
|
||||||
,[DateFinalized]
|
|
||||||
,[InvoiceBalance]
|
|
||||||
FROM inv1
|
|
||||||
UNION
|
|
||||||
SELECT [Id] = cast([id] as varchar(15))
|
|
||||||
,[PaymentTerm] = '10wd'
|
|
||||||
,[SendToEmail] = sy.[SupportMail]
|
|
||||||
,[SendToAddress] = (select TOP(1) sy.[name] + CHAR(10) + l.[addressString] + CHAR(10) + l.[postal] + ' ' + l.[city] + CHAR(10) + l.[country] from [dbo].[mfr__#locations] as l where l.[EntityId] = sy.[CustomerId])
|
|
||||||
,[CustomerId]
|
|
||||||
,[Invoiceid]
|
|
||||||
,[DateFinalized] = [DateOfCreation]
|
|
||||||
,[InvoiceBalance]
|
|
||||||
FROM [dbo].[mfr__invoices] as mfri
|
|
||||||
LEFT JOIN (SELECT TOP(1) [invid] = p.[PartnerId], s.[CustomerId], cy.[supportmail], cy.[Name]
|
|
||||||
FROM dbo.[mfr__*PartnerSet] as p
|
|
||||||
JOIN [dbo].[mfr__servicerequests] as s on p.[EntityId] = s.[Id] AND p.[Property] = 'ServiceRequest:Invoices'
|
|
||||||
JOIN [dbo].[mfr__companies] as cy on s.[CustomerId] = cy.id
|
|
||||||
WHERE p.PartnerId = TRY_CAST(@invId as bigint)) as sy on mfri.Id = sy.[invid]
|
|
||||||
WHERE [id] = TRY_CAST(@invId as bigint) AND NOT EXISTS( SELECT 0 FROM inv1 ) --will only be used, if id not present in fds__invoices
|
|
||||||
AND [Filetype] not in ('PdfCancelInvoice')
|
|
||||||
AND [invoiceState] not in ('eIsCancelled','eIsPaid')
|
|
||||||
)
|
|
||||||
SELECT TOP(1)
|
|
||||||
[today] = @today
|
|
||||||
, [invid] = inv.[id]
|
|
||||||
, [sender] = N'Sebastian Fuchs GmbH & Co. KG ● Germaniastraße 15 ● 40223 Düsseldorf'
|
|
||||||
, [paymentterms] = [PaymentTerm]
|
|
||||||
, [invoiceemail] = [SendToemail]
|
|
||||||
, [invoiceaddress] = [SendToAddress]
|
|
||||||
, [CustomerId] = [CustomerId]
|
|
||||||
, [subject] = CASE WHEN @type = 'f' THEN N'Zahlungserinnerung'
|
|
||||||
WHEN @type = 'm' THEN FORMAT(@level,'0','de-de') + N'. Mahnung'
|
|
||||||
WHEN @type = 'l' THEN N'Letzte außergerichtliche Mahnung'
|
|
||||||
ELSE N'Zahlungserinnerung'
|
|
||||||
END
|
|
||||||
, [type] = @type
|
|
||||||
, [invoiceid] = inv.[Invoiceid]
|
|
||||||
, [invoicedate] = inv.[DateFinalized]
|
|
||||||
, [amount] = inv.[InvoiceBalance]
|
|
||||||
, [amount_payed] = CAST( ISNULL([dbo].[fds__fn_InvoicePaymentAmount](inv.[Id]),0.0) as numeric(10,2))
|
|
||||||
FROM inv;
|
|
||||||
|
|
||||||
|
|
||||||
END
|
|
||||||
@@ -1,459 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__prepStorno_recreate]
|
|
||||||
@InvId varchar(20)
|
|
||||||
, @authuser varchar(100)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
IF [dbo].[fis_getModuleAuth]('fds_inv', @authuser) < 2
|
|
||||||
THROW 60000, N'not authorized', 1;
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info])
|
|
||||||
VALUES ('fds__prepStorno_recreate' ,@authuser , '');
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @today date = GETDATE();
|
|
||||||
DECLARE @type char(1) = 'r';
|
|
||||||
|
|
||||||
DECLARE @selReq as TABLE ([id] bigint);
|
|
||||||
IF EXISTS (SELECT 0 FROM [dbo].[fds__invoices] WHERE [id] = @InvId AND ISNULL([isfinal],0) = 1)
|
|
||||||
BEGIN
|
|
||||||
INSERT INTO @selReq SELECT DISTINCT irq.[mfr__servicerequest] from [dbo].[fds__invoice_servicerequests] as irq where InvId = @InvId;
|
|
||||||
END
|
|
||||||
ELSE IF EXISTS (SELECT 0 FROM [dbo].[mfr__invoices] as _i where TRY_CAST(_i.[Id] as varchar(20)) = @InvId AND [FileType] in ('PdfInvoice','PdfPartialInvoice'))
|
|
||||||
BEGIN
|
|
||||||
INSERT INTO @selReq SELECT DISTINCT ps.[PartnerId] from [dbo].[mfr__invoices] as _i JOIN [mfr__*PartnerSet] as ps ON _i.[Id] = ps.[EntityId] and ps.[Property] = 'Invoice:SourceServiceRequest' where TRY_CAST(_i.Id as varchar(20)) = @InvId and EXISTS ( SELECT 0 FROM [dbo].[mfr__servicerequests] as _s WHERE _s.[Id] = ps.[PartnerId]);
|
|
||||||
END
|
|
||||||
|
|
||||||
DECLARE @SReq TABLE (
|
|
||||||
[Id] [bigint] NULL,
|
|
||||||
[Name] [nvarchar](255) NULL,
|
|
||||||
[ExternalId] [nvarchar](255) NULL,
|
|
||||||
[InvoiceId] [nvarchar](255) NULL,
|
|
||||||
[ClosedAt] [datetime] NULL,
|
|
||||||
[ReleasedAt] [datetime] NULL,
|
|
||||||
[WorkDoneAt] [datetime] NULL,
|
|
||||||
[TargetTimeInMinutes] [bigint] NULL,
|
|
||||||
[DateModified] [datetime] NULL,
|
|
||||||
[DateOfCreation] [datetime] NULL,
|
|
||||||
[DueDateRangeStart] [datetime] NULL,
|
|
||||||
[DueDateRangeEnd] [datetime] NULL,
|
|
||||||
[PortalLink] [nvarchar](255) NULL,
|
|
||||||
[CostCenterId] [bigint] NULL,
|
|
||||||
[Description] [nvarchar](max) NULL,
|
|
||||||
[State] [nvarchar](255) NULL,
|
|
||||||
[CustomValues] [nvarchar](max) NULL,
|
|
||||||
[CurrentOwnerId] [bigint] NULL,
|
|
||||||
[CustomerId] [bigint] NULL,
|
|
||||||
[ParentServiceRequestId] [bigint] NULL,
|
|
||||||
[ParentExtenalId] [nvarchar](255) NULL,
|
|
||||||
[ParentWorkDoneAt] [date] NULL,
|
|
||||||
[ParentInvoiceId] [nvarchar](255) NULL,
|
|
||||||
[Location#ID] [bigint] NULL,
|
|
||||||
[Version] [int] NULL,
|
|
||||||
[IsTemplate] [bit] NULL,
|
|
||||||
[IsTemplateMobile] [bit] NULL,
|
|
||||||
[CreateFromServiceRequestTemplateId] [bigint] NULL,
|
|
||||||
[Type] [nvarchar](255) NULL,
|
|
||||||
[Summary] nvarchar(255) NULL,
|
|
||||||
[isChild] bit NULL,
|
|
||||||
[baseorder] int,
|
|
||||||
[provision] varchar(255) NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
insert into @SReq
|
|
||||||
SELECT
|
|
||||||
[Id]
|
|
||||||
,[Name]
|
|
||||||
,[ExternalId]
|
|
||||||
,[InvoiceId]
|
|
||||||
,[ClosedAt]
|
|
||||||
,[ReleasedAt]
|
|
||||||
,[WorkDoneAt]
|
|
||||||
,[TargetTimeInMinutes]
|
|
||||||
,[DateModified]
|
|
||||||
,[DateOfCreation]
|
|
||||||
,[DueDateRangeStart]
|
|
||||||
,[DueDateRangeEnd]
|
|
||||||
,[PortalLink]
|
|
||||||
,[CostCenterId]
|
|
||||||
,[Description]
|
|
||||||
,[State]
|
|
||||||
,[CustomValues]
|
|
||||||
,[CurrentOwnerId]
|
|
||||||
,[CustomerId]
|
|
||||||
,[ParentServiceRequestId] = CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then [id] ELSE s.[ParentServiceRequestId] END
|
|
||||||
,[ParentExtenalId] = cast(CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then null ELSE '?' END as nvarchar(255))
|
|
||||||
,[ParentWorkDoneAt] = cast(NULL as datetime)
|
|
||||||
,[ParentInvoiceId] = cast(NULL as nvarchar(255))
|
|
||||||
,[Location#ID]
|
|
||||||
,[Version]
|
|
||||||
,[IsTemplate]
|
|
||||||
,[IsTemplateMobile]
|
|
||||||
,[CreateFromServiceRequestTemplateId]
|
|
||||||
,[Type]
|
|
||||||
,[Summary] = NULL
|
|
||||||
,[isChild] = CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then 0 ELSE 1 END
|
|
||||||
,[baseorder] = 0
|
|
||||||
,[provision] = NULL
|
|
||||||
FROM [dbo].[mfr__servicerequests] as s
|
|
||||||
WHERE [id] IN (SELECT [Id] from @selReq);
|
|
||||||
|
|
||||||
IF @type <> 'i' -- for interim invoices, only one selected servicerequest is allowed -> childs and parents are ignored
|
|
||||||
BEGIN
|
|
||||||
INSERT INTO @SReq
|
|
||||||
SELECT s.[Id]
|
|
||||||
,s.[Name]
|
|
||||||
,s.[ExternalId]
|
|
||||||
,s.[InvoiceId]
|
|
||||||
,s.[ClosedAt]
|
|
||||||
,s.[ReleasedAt]
|
|
||||||
,s.[WorkDoneAt]
|
|
||||||
,s.[TargetTimeInMinutes]
|
|
||||||
,s.[DateModified]
|
|
||||||
,s.[DateOfCreation]
|
|
||||||
,s.[DueDateRangeStart]
|
|
||||||
,s.[DueDateRangeEnd]
|
|
||||||
,s.[PortalLink]
|
|
||||||
,s.[CostCenterId]
|
|
||||||
,s.[Description]
|
|
||||||
,s.[State]
|
|
||||||
,s.[CustomValues]
|
|
||||||
,s.[CurrentOwnerId]
|
|
||||||
,s.[CustomerId]
|
|
||||||
,s.[ParentServiceRequestId]
|
|
||||||
,[ParentExtenalId] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[ExternalId] ELSE NULL END
|
|
||||||
,[ParentWorkDoneAt] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[WorkDoneAt] ELSE NULL END
|
|
||||||
,[ParentInvoiceId] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[InvoiceId] ELSE NULL END
|
|
||||||
,s.[Location#ID]
|
|
||||||
,s.[Version]
|
|
||||||
,s.[IsTemplate]
|
|
||||||
,s.[IsTemplateMobile]
|
|
||||||
,s.[CreateFromServiceRequestTemplateId]
|
|
||||||
,s.[Type]
|
|
||||||
,[Summary] = NULL
|
|
||||||
,[isChild] = CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then 0 ELSE 1 END
|
|
||||||
,[baseorder] = 0
|
|
||||||
, [provision_start] = null
|
|
||||||
from [dbo].[mfr__servicerequests] as s JOIN @SReq as q ON (s.[ParentServiceRequestId] = q.[id] OR q.[ParentServiceRequestId] = s.[id]) --parents or childs
|
|
||||||
WHERE NOT EXISTS (SELECT * FROM @SReq as s2 WHERE s2.[id] = s.[Id])
|
|
||||||
END;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
WITH q as (
|
|
||||||
SELECT [id], [baseorder] = (ROW_NUMBER() OVER (ORDER BY ISNULL([ParentWorkDoneAt], [WorkDoneAt]), ISNULL([ParentServiceRequestId],[id])) * 100)
|
|
||||||
FROM @SReq as s
|
|
||||||
WHERE [ischild] = 0
|
|
||||||
)
|
|
||||||
UPDATE s SET
|
|
||||||
[baseorder] = q.baseorder
|
|
||||||
FROM @SReq as s JOIN q ON s.[id] = q.[id];
|
|
||||||
|
|
||||||
|
|
||||||
WITH q as (
|
|
||||||
SELECT s.[id]
|
|
||||||
, q.[ExternalId], q.[WorkDoneAt], q.[InvoiceId]
|
|
||||||
, [baseorder] = q.[baseorder] + ROW_NUMBER() OVER (PARTITION BY q.[id] ORDER BY s.[dateofcreation])
|
|
||||||
FROM @SReq as s JOIN @SReq as q ON s.[ParentServiceRequestId] = q.[id]
|
|
||||||
WHERE s.[ischild] = 1
|
|
||||||
)
|
|
||||||
UPDATE s SET
|
|
||||||
[ParentExtenalId] = q.[ExternalId]
|
|
||||||
,[ParentWorkDoneAt] = q.[WorkDoneAt]
|
|
||||||
,[ParentInvoiceId] = q.[InvoiceId]
|
|
||||||
,[baseorder] = q.[baseorder]
|
|
||||||
FROM @SReq as s JOIN q ON s.[Id] = q.[id];
|
|
||||||
|
|
||||||
IF @type <> 'i'
|
|
||||||
BEGIN
|
|
||||||
DELETE s FROM @SReq as s WHERE NOT EXISTS(SELECT * FROM @selReq as sel where sel.[id] = s.[Id])
|
|
||||||
END
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @step [dbo].[fds__tt__mfr_steps];
|
|
||||||
DECLARE @itm [dbo].[fds__tt__mfr_items];
|
|
||||||
DECLARE @company TABLE (
|
|
||||||
[Id] [bigint] NOT NULL,
|
|
||||||
[Location#ID] [bigint] NULL,
|
|
||||||
[Version] [int] NOT NULL,
|
|
||||||
[IsPhysicalPerson] [bit] NOT NULL,
|
|
||||||
[IsOwner] [bit] NOT NULL,
|
|
||||||
[IsEmailInvoicingActive] [bit] NOT NULL,
|
|
||||||
[IsSupplier] [bit] NOT NULL,
|
|
||||||
[MappingId] [nvarchar](255) NULL,
|
|
||||||
[ExternalId] [nvarchar](255) NULL,
|
|
||||||
[Name] [nvarchar](255) NULL,
|
|
||||||
[Note] [nvarchar](255) NULL,
|
|
||||||
[SupportTelephone] [nvarchar](255) NULL,
|
|
||||||
[SupportFax] [nvarchar](255) NULL,
|
|
||||||
[SupportMail] [nvarchar](255) NULL,
|
|
||||||
[QuickSearch] [nvarchar](255) NULL,
|
|
||||||
[DateModified] [datetime] NOT NULL,
|
|
||||||
[CustomValues] [nvarchar](max) NOT NULL,
|
|
||||||
[MainContactId] [bigint] NOT NULL,
|
|
||||||
[address] nvarchar(1000) null
|
|
||||||
);
|
|
||||||
INSERT INTO @company SELECT DISTINCT cy.*, [address] = [dbo].[fds__getCompanyAddress](cy.[id]) FROM [mfr__companies] as cy JOIN @SReq as s on cy.id = s.[CustomerId];
|
|
||||||
DECLARE @companyids [dbo].[fds__tt__idlist];
|
|
||||||
INSERT INTO @companyids SELECT DISTINCT [id] FROM @company;
|
|
||||||
|
|
||||||
|
|
||||||
INSERT INTO @itm
|
|
||||||
SELECT itm.*, [baseorder] = s.[baseorder], [order] = ROW_NUMBER() OVER (PARTITION BY [ServiceRequestId] ORDER BY [SortOrder], itm.[Id])
|
|
||||||
from [dbo].[mfr__items] as itm JOIN @SReq as s ON itm.ServiceRequestId = s.[Id];
|
|
||||||
|
|
||||||
DECLARE @NUM_of_int_Invoices int = 0;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- Add virtual items for intermediate invoices
|
|
||||||
INSERT INTO @itm
|
|
||||||
SELECT
|
|
||||||
[Id] = -2
|
|
||||||
,[Version] = 0
|
|
||||||
,[QuantityHours] = 1
|
|
||||||
,[PlannedQuantityHours] = 1
|
|
||||||
,[ItemTypeId] = NULL
|
|
||||||
,[ItemNumber] = NULL
|
|
||||||
,[TrackingId] = NULL
|
|
||||||
,[Manufacture] = NULL
|
|
||||||
,[Price] = inv.InvoiceBalance_net * -1.0 --without VAT
|
|
||||||
,[Costs] = inv.InvoiceBalance_net * -1.0 --without VAT
|
|
||||||
,[NameOrNumber] = REPLACE(inv.DocumentName, '.pdf','')
|
|
||||||
,[CustomValues] = NULL
|
|
||||||
,[Note] = '<p>' + 'Anrechnung der ' + REPLACE(inv.DocumentName, '.pdf','') + ' vom ' + FORMAT(inv.[dateFinalized], 'dd.MM.yyyy', 'de-de') + '</p>'
|
|
||||||
,[ExternalId] = NULL
|
|
||||||
,[Discount] = NULL
|
|
||||||
,[VAT] = inv.InvoiceVAT_1
|
|
||||||
,[IsManual] = 1
|
|
||||||
,[SortOrder] = ROW_NUMBER() OVER (PARTITION BY srq.[mfr__servicerequest] ORDER BY inv.[dateFinalized], inv.DateCreated)
|
|
||||||
,[Type] = 'PartialInvoice'
|
|
||||||
,[ServiceRequestId] = -2 --s.[id]
|
|
||||||
,[ServiceObjectId] = Null
|
|
||||||
,[CreatorId] = NULL
|
|
||||||
,[UnitId] = 15825600513
|
|
||||||
,[UnitString] = 'Pauschal'
|
|
||||||
,[baseorder] = s.[baseorder], [order] = 10000 + ROW_NUMBER() OVER (PARTITION BY srq.[mfr__servicerequest] ORDER BY inv.[dateFinalized], inv.DateCreated)
|
|
||||||
from [dbo].[fds__invoices] as inv
|
|
||||||
JOIN [dbo].[fds__invoice_servicerequests] as srq ON inv.[Id] = srq.[InvId] AND inv.[InvoiceType] = 'i' --important
|
|
||||||
JOIN @SReq as s ON srq.[mfr__servicerequest] = s.[Id]
|
|
||||||
WHERE inv.[InvoiceType] = 'i' --important;
|
|
||||||
AND ISNULL(inv.[isFinal],0) = 1 AND ISNULL(inv.[IsCanceled],0) = 0
|
|
||||||
AND [dbo].[fds__fn_IntermediateIsAllocatedToOther](inv.[id],@InvId) = 0; --inonly those intermediates that are not already allocated to any other (than the to be replaced Inv);
|
|
||||||
|
|
||||||
IF EXISTS (SELECT * FROM @itm WHERE [ServiceRequestId] = -2)
|
|
||||||
BEGIN
|
|
||||||
-- Add virtual request
|
|
||||||
INSERT INTO @SReq ([Id]
|
|
||||||
,[Name]
|
|
||||||
,[Type]
|
|
||||||
,[isChild]
|
|
||||||
,[baseorder])
|
|
||||||
VALUES(-2
|
|
||||||
,'Abschlagszahlungen'
|
|
||||||
,'vrii'
|
|
||||||
,0
|
|
||||||
,999);
|
|
||||||
|
|
||||||
SET @type = 'f';
|
|
||||||
|
|
||||||
END;
|
|
||||||
|
|
||||||
|
|
||||||
-- Add virtual item (to front) for (to-be) replaced invoice
|
|
||||||
INSERT INTO @itm
|
|
||||||
SELECT TOP(1) --can only be one
|
|
||||||
[Id] = -2
|
|
||||||
,[Version] = 0
|
|
||||||
,[QuantityHours] = 1
|
|
||||||
,[PlannedQuantityHours] = 1
|
|
||||||
,[ItemTypeId] = NULL
|
|
||||||
,[ItemNumber] = NULL
|
|
||||||
,[TrackingId] = NULL
|
|
||||||
,[Manufacture] = NULL
|
|
||||||
,[Price] = inv.InvoiceBalance_net * -1.0 --without VAT
|
|
||||||
,[Costs] = inv.InvoiceBalance_net * -1.0 --without VAT
|
|
||||||
,[NameOrNumber] = REPLACE(inv.DocumentName, '.pdf','')
|
|
||||||
,[CustomValues] = NULL
|
|
||||||
,[Note] = '<p>' + 'Storno der ' + REPLACE(inv.DocumentName, '.pdf','') + ' vom ' + FORMAT(inv.[dateFinalized], 'dd.MM.yyyy', 'de-de') + '</p>'
|
|
||||||
,[ExternalId] = NULL
|
|
||||||
,[Discount] = NULL
|
|
||||||
,[VAT] = inv.InvoiceVAT_1
|
|
||||||
,[IsManual] = 1
|
|
||||||
,[SortOrder] = 1
|
|
||||||
,[Type] = 'CanceledInvoice'
|
|
||||||
,[ServiceRequestId] = -3 --s.[id]
|
|
||||||
,[ServiceObjectId] = Null
|
|
||||||
,[CreatorId] = NULL
|
|
||||||
,[UnitId] = 15825600513
|
|
||||||
,[UnitString] = 'Pauschal'
|
|
||||||
,[baseorder] = 0, [order] = 1
|
|
||||||
from [dbo].[fds__invoices] as inv
|
|
||||||
WHERE ISNULL(inv.[isFinal],0) = 1 AND ISNULL(inv.[IsCanceled],0) = 0
|
|
||||||
AND inv.[Id] = @InvId;
|
|
||||||
|
|
||||||
IF EXISTS (SELECT * FROM @itm WHERE [ServiceRequestId] = -3)
|
|
||||||
BEGIN
|
|
||||||
-- Add virtual request
|
|
||||||
INSERT INTO @SReq ([Id]
|
|
||||||
,[Name]
|
|
||||||
,[Type]
|
|
||||||
,[isChild]
|
|
||||||
,[baseorder])
|
|
||||||
VALUES(-3
|
|
||||||
,'Storno'
|
|
||||||
,'vrci'
|
|
||||||
,0
|
|
||||||
,-999);
|
|
||||||
|
|
||||||
END;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** OUTPUT + get other types **/
|
|
||||||
|
|
||||||
|
|
||||||
WITH app as(
|
|
||||||
SELECT [start] = MIN(CAST(CASE WHEN a.[StartDateTime] is null THEN s.[workdoneat] WHEN CAST(a.[StartDateTime] as date) > s.[workdoneat] THEN CAST(a.[StartDateTime] as date) ELSE s.[workdoneat] END as date))
|
|
||||||
, [end] = MAX(CAST(CASE WHEN a.[EndDateTime] is null THEN s.[workdoneat] WHEN CAST(a.[EndDateTime] as date) > s.[workdoneat] THEN CAST(a.[EndDateTime] as date) ELSE s.[workdoneat] END as date))
|
|
||||||
, [CustomerId] = MIN (s.customerid)
|
|
||||||
FROM [dbo].[mfr__appointments] as a
|
|
||||||
RIGHT JOIN @SReq as s on a.[ServiceRequestId] = s.[Id] AND a.[State] in ('IsWorkDone')
|
|
||||||
)
|
|
||||||
SELECT TOP(1)
|
|
||||||
[today]
|
|
||||||
, [provisionstart] = CASe WHEN @type = 'i' THEN NULL WHEN [start] = [end] THEN NULL ELSE [start] END
|
|
||||||
, [provisionend] = CASe WHEN @type = 'i' THEN @today ELSE [end] END -- Abschlagsrechnungen immer mit Tagesdatum
|
|
||||||
, [provisionlocation] = ISNULL((SELECT TOP(1) [provisionlocation] FROM [dbo].[fds__invoices] where [Id] = @InvId and ISNULL([provisionlocation],'') <>''),N'')
|
|
||||||
, [sender] = N'Sebastian Fuchs GmbH & Co. KG ● Germaniastraße 15 ● 40223 Düsseldorf'
|
|
||||||
, [paymentterms] = ISNULL((SELECT TOP(1) [PaymentTerm] FROM [dbo].[fds__invoices] where [Id] = @InvId and ISNULL([PaymentTerm],'') <>''),N'10wd')
|
|
||||||
, [invoiceemail] = ISNULL((SELECT TOP(1) [SendToemail] FROM [dbo].[fds__invoices] where [Id] = @InvId and ISNULL([sendToEmail],'') <>''),(SELECT TOP(1) [SupportMail] FROM @company where IsEmailInvoicingActive = 1))
|
|
||||||
, [invoiceaddress] = ISNULL((SELECT TOP(1) [SendToAddress] FROM [dbo].[fds__invoices] where [Id] = @InvId and ISNULL([SendToAddress],'') <>''),(SELECT TOP(1) CONCAT([name], CHAR(10), [address]) FROM @company ORDER BY IsEmailInvoicingActive DESC))
|
|
||||||
, [tax_servicerefund] = 0.2
|
|
||||||
, [CustomerId] = [CustomerId]
|
|
||||||
, [invoicetitle] = CASE WHEN @type = 'i' THEN (CASE WHEN @NUM_of_int_Invoices > 0 THEN CAST((@NUM_of_int_Invoices + 1) as varchar(3)) + '. ' ELSE '' END) + 'Abschlagsrechnung'
|
|
||||||
WHEN @type = 'f' THEN 'Schlussrechnung' WHEN @type = 'c' THEN 'Stornorechnung' ELSE 'Rechnung' END
|
|
||||||
, [type] = @type
|
|
||||||
FROM (VALUES(CAST(GETDATE() as date)))base ([today]) CROSS JOIN app;
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
*
|
|
||||||
,[order] = ROW_NUMBER() OVER (ORDER BY [baseorder])
|
|
||||||
FROM @SReq;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
IF @type <> 'i'
|
|
||||||
BEGIN
|
|
||||||
SELECT
|
|
||||||
itm.[Id]
|
|
||||||
, [net] = ISNULL([Price],0)
|
|
||||||
, [quantityhours] = ISNULL([quantityhours],1)
|
|
||||||
, itm.[UnitString]
|
|
||||||
, [net_val] = CAST( (ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0) as numeric(7,2))
|
|
||||||
, [bo_val] = CAST( ((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) * (1 + (ISNULL(vat.[vat], 19.0) * 0.01)) as numeric(7,2))
|
|
||||||
, [vat] = CASE WHEN LOWER(itm.[type]) in ('Title','Text') THEN null ELSE FORMAT(ISNULL(vat.[vat], 19.0), '#0.0','de') + '%' END
|
|
||||||
, [vat_val] = CAST( ((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) * ((ISNULL(vat.[vat], 19.0) * 0.01)) as numeric(7,2))
|
|
||||||
, itm.[ServiceRequestId]
|
|
||||||
, itm.[SortOrder]
|
|
||||||
, itm.[Type]
|
|
||||||
, itm.[NameOrNumber]
|
|
||||||
, itm.[Note]
|
|
||||||
, [order] = (itm.[baseorder] * 100) + itm.[order]
|
|
||||||
FROM @itm as itm
|
|
||||||
LEFT JOIN [dbo].[fds__custom_vat] as vat ON itm.[Id] = vat.[EntityId] and vat.[EntityType] = 'item';
|
|
||||||
END
|
|
||||||
ELSE
|
|
||||||
BEGIN
|
|
||||||
WITH citm as (
|
|
||||||
SELECT itm.[ServiceRequestId], [net_val] = CAST( SUM( (ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0) ) as numeric(7,2))
|
|
||||||
FROM @itm as itm
|
|
||||||
WHERE itm.[Type] = 'Material'
|
|
||||||
GROUP BY itm.[ServiceRequestId]
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
[Id] = -1
|
|
||||||
, [net] = 1000.0
|
|
||||||
, [quantityhours] = 1
|
|
||||||
, [UnitString] = 'Pauschal'
|
|
||||||
, [net_val] = CAST( 1000.0 as numeric(7,2))
|
|
||||||
, [bo_val] = CAST( 1000.0 * 1.19 as numeric(7,2))
|
|
||||||
, [vat] = FORMAT( 19.0, '#0.0','de') + '%'
|
|
||||||
, [vat_val] = CAST( 1000.0 * 0.19 as numeric(7,2))
|
|
||||||
, [ServiceRequestId] = s.[id]
|
|
||||||
, [SortOrder] = 0
|
|
||||||
, [Type] = 'PartialInvoice'
|
|
||||||
, [NameOrNumber] = 'Abschlagsbetrag für bereits erbrachte Arbeiten, Dienstleistungen, Materiallieferungen und getätigte Bestellvorgänge zum oben genannten Bauvorhaben'
|
|
||||||
, [Note] = ''
|
|
||||||
, [order] = 1
|
|
||||||
FROM @SReq as s;
|
|
||||||
|
|
||||||
END
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
INSERT INTO @step
|
|
||||||
SELECT
|
|
||||||
stp.*
|
|
||||||
, s.[baseorder]
|
|
||||||
, [order] = (s.[baseorder] * 100) + ROW_NUMBER() OVER (PARTITION BY stp.[ServiceRequestId] ORDER BY stp.[SortOrder], stp.[Id])
|
|
||||||
FROM [dbo].[mfr__steps] as stp JOIN @SReq as s ON stp.ServiceRequestId = s.[Id];
|
|
||||||
SELECT * from @step;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
cc.[Id]
|
|
||||||
, [CompanyName] = cc.[name]
|
|
||||||
, ct.[Gender]
|
|
||||||
, ct.[FirstName]
|
|
||||||
, ct.[LastName]
|
|
||||||
, ct.[Email]
|
|
||||||
, cc.[address]
|
|
||||||
FROM [dbo].[fds__getCompanysContacts](@companyids) as ct join @company as cc on cc.[Id] = ct.[CyId];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
SELECT DISTINCT
|
|
||||||
[ref] = 'Einsatzort ' + so.[name]
|
|
||||||
,[address] = [dbo].[fds__fn_combineAddress] (so.[Name], loc.[AddressString], null
|
|
||||||
,loc.[Postal]
|
|
||||||
,loc.[City]
|
|
||||||
,loc.[State]
|
|
||||||
,loc.[Country])
|
|
||||||
FROM @SReq as s
|
|
||||||
LEFT JOIN [mfr__*PartnerSet] as p ON p.property = 'ServiceRequest:ServiceObjects' and p.[EntityId] = s.[id]
|
|
||||||
LEFT JOIN [mfr__serviceobjects] as so ON so.id = p.[PartnerId]
|
|
||||||
LEFT JOIN [mfr__*PartnerSet] as po ON p.property = 'ServiceObject:Location' and po.[EntityId] = so.[Id]
|
|
||||||
LEFT join [dbo].[mfr__#locations] as loc ON (loc.[id] = so.[Location#ID] OR po.[PartnerId] = loc.[Id] OR (so.[Id] = loc.[EntityId] and loc.Property = 'ServiceObject:Location'))
|
|
||||||
WHERE so.id is not null and loc.[id] is not null
|
|
||||||
UNION
|
|
||||||
SELECT DISTINCT
|
|
||||||
[ref] = 'Auftragsort ' + [name]
|
|
||||||
,[address] = [dbo].[fds__fn_combineAddress] (loc.[AddressString], null, null
|
|
||||||
,loc.[Postal]
|
|
||||||
,loc.[City]
|
|
||||||
,loc.[State]
|
|
||||||
,loc.[Country])
|
|
||||||
FROM @SReq as s
|
|
||||||
LEFT JOIN [mfr__*PartnerSet] as p ON p.property = 'ServiceRequest:Location' and p.[EntityId] = s.[id]
|
|
||||||
join [dbo].[mfr__#locations] as loc ON (s.[Location#ID] = loc.[id] OR p.[PartnerId] = loc.[Id] OR (s.[Id] = loc.[EntityId] and loc.Property = 'ServiceRequest:Location'))
|
|
||||||
WHERE loc.[id] is not null
|
|
||||||
UNION
|
|
||||||
SELECT DISTINCT [ref] = 'Kunde'
|
|
||||||
, [address]
|
|
||||||
FROM @company;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
END
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__r_getBalanceByMonth]
|
|
||||||
@authuser varchar(100)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
|
|
||||||
DECLARE @today date = GETDATE()
|
|
||||||
DECLARE @tomorrow date = DATEADD(DAY,1,@today);
|
|
||||||
|
|
||||||
IF [dbo].[fis_getModuleAuth]('fds_reports', @authuser) < 2
|
|
||||||
THROW 60000, N'not authorized', 1;
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @Admin [dbo].[fds__tt__admin_ReportAdminTable];
|
|
||||||
|
|
||||||
--Admin Table
|
|
||||||
INSERT INTO @Admin
|
|
||||||
VALUES (
|
|
||||||
'bbm'
|
|
||||||
,'Umsatz nach Monaten'
|
|
||||||
,'Umsatz nach Monaten'
|
|
||||||
,'' + FORMAT(GETDATE(), 'yyyy-MM-dd HH:mm')
|
|
||||||
,''
|
|
||||||
,'table' --typ
|
|
||||||
, '' --settings
|
|
||||||
, ''
|
|
||||||
, 0)
|
|
||||||
|
|
||||||
SELECT * FROM @Admin;
|
|
||||||
|
|
||||||
WITH inv as (
|
|
||||||
SELECT
|
|
||||||
i.[Dateofcreation]
|
|
||||||
,i.[InvoiceBalance]
|
|
||||||
,i.[InvoiceBalanceNetto]
|
|
||||||
FROM [dbo].[mfr__invoices] as i
|
|
||||||
where --i.[invoicestate] in('eIsSent','eIsPaid','eIsOpen') and
|
|
||||||
i.[FileType] in( 'PdfInvoice','PdfCancelInvoice','PdfPartialInvoice') and ISNULL(InvoiceId,'') not in ('Preview','')
|
|
||||||
UNION
|
|
||||||
SELECT
|
|
||||||
[Dateofcreation] = i.[DateCreatEd]
|
|
||||||
,i.[InvoiceBalance]
|
|
||||||
,[InvoiceBalanceNetto] = i.[InvoiceBalance_net]
|
|
||||||
FROM [dbo].[fds__invoices] as i
|
|
||||||
WHERE i.[isFinal] = 1
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
[Monat] = FORMAT( inv.[Dateofcreation], 'yyyy - MM', 'de')
|
|
||||||
, [∑ Umsätze] = FORMAT( SUM( ISNULL(inv.[InvoiceBalance],0) ), '#,0.00 €', 'de')
|
|
||||||
, [style:∑ Umsätze] = 'text-align: right'
|
|
||||||
, [∑ Umsätze_netto] = FORMAT( SUM( ISNULL(inv.[InvoiceBalanceNetto],0) ), '#,0.00 €', 'de')
|
|
||||||
, [style:∑ Umsätze_netto] = 'text-align: right'
|
|
||||||
FROM inv
|
|
||||||
GROUP BY FORMAT( inv.[Dateofcreation], 'yyyy - MM', 'de');
|
|
||||||
|
|
||||||
END
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
|
|
||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__r_getBalanceByYearTopCustomer]
|
|
||||||
@year int,
|
|
||||||
|
|
||||||
@authuser varchar(100)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
|
|
||||||
DECLARE @today date = GETDATE()
|
|
||||||
DECLARE @tomorrow date = DATEADD(DAY,1,@today);
|
|
||||||
|
|
||||||
IF [dbo].[fis_getModuleAuth]('fds_reports', @authuser) < 2
|
|
||||||
THROW 60000, N'not authorized', 1;
|
|
||||||
|
|
||||||
DECLARE @Admin [dbo].[fds__tt__admin_ReportAdminTable];
|
|
||||||
|
|
||||||
--Admin Table
|
|
||||||
INSERT INTO @Admin
|
|
||||||
VALUES (
|
|
||||||
'bbm'
|
|
||||||
,'Umsatz der Top 10 Kunden für Jahr ' + CAST( @year as varchar(10))
|
|
||||||
,'Umsatz der Top 10 Kunden pro Jahr ' + CAST( @year as varchar(10))
|
|
||||||
,'' + FORMAT(GETDATE(), 'yyyy-MM-dd HH:mm')
|
|
||||||
,''
|
|
||||||
,'table' --typ
|
|
||||||
, '' --settings
|
|
||||||
, ''
|
|
||||||
, 0);
|
|
||||||
|
|
||||||
SELECT * FROM @Admin;
|
|
||||||
|
|
||||||
|
|
||||||
WITH inv as(
|
|
||||||
select
|
|
||||||
[InvoiceBalance] = SUM( ISNULL(_i.[InvoiceBalance],0) )
|
|
||||||
,[Dateofcreation] = CAST(_i.[Dateofcreation] as date)
|
|
||||||
,srq.CustomerId
|
|
||||||
,[customerName] = cy.Name
|
|
||||||
,[seq] = ROW_NUMBER() OVER (ORDER BY CAST(_i.[Dateofcreation] as date))
|
|
||||||
FROM [dbo].[mfr__invoices] as _i
|
|
||||||
JOIN [dbo].[mfr__servicerequests] as srq on srq.InvoiceId like ('%' + _i.[InvoiceId] + '%')
|
|
||||||
JOIN [dbo].[mfr__companies] as cy on srq.CustomerId = cy.id
|
|
||||||
where _i.[invoicestate] in('eIsSent','eIsPaid','eIsOpen') and YEAR(_i.[Dateofcreation]) = @year
|
|
||||||
GROUP BY CAST(_i.[Dateofcreation] as date), CustomerId, cy.name
|
|
||||||
UNION
|
|
||||||
select
|
|
||||||
[InvoiceBalance] = SUM( ISNULL(_i.[InvoiceBalance],0) )
|
|
||||||
,[Dateofcreation] = CAST(_i.[DateCreated] as date)
|
|
||||||
,_i.CustomerId
|
|
||||||
,[customerName] = cy.Name
|
|
||||||
,[seq] = ROW_NUMBER() OVER (ORDER BY CAST(_i.[DateCreated] as date))
|
|
||||||
FROM [dbo].[fds__invoices] as _i
|
|
||||||
JOIN [dbo].[mfr__companies] as cy on _i.CustomerId = cy.id
|
|
||||||
where YEAR(_i.[DateCreated]) = @year and _i.[isfinal] = 1
|
|
||||||
GROUP BY CAST(_i.[DateCreated] as date), CustomerId, cy.name
|
|
||||||
), yr1 as (
|
|
||||||
SELECT
|
|
||||||
[year] = YEAR( inv.[Dateofcreation] )
|
|
||||||
, [total] = SUM( ISNULL(inv.[InvoiceBalance],0) )
|
|
||||||
FROM [dbo].[mfr__invoices] as inv
|
|
||||||
where inv.[invoicestate] in('eIsSent','eIsPaid','eIsOpen')
|
|
||||||
GROUP BY YEAR( inv.[Dateofcreation])
|
|
||||||
UNION
|
|
||||||
SELECT
|
|
||||||
[year] = YEAR( _i.[DateCreated] )
|
|
||||||
, [total] = SUM( ISNULL(_i.[InvoiceBalance],0) )
|
|
||||||
FROM [dbo].[fds__invoices] as _i
|
|
||||||
WHERE _i.[isfinal] = 1
|
|
||||||
GROUP BY YEAR( _i.[DateCreated])
|
|
||||||
), yr as (
|
|
||||||
SELECT [year], [total] = SUM([total])
|
|
||||||
from yr1 GROUP BY [year]
|
|
||||||
)
|
|
||||||
SELECT TOP(10)
|
|
||||||
[Jahr] = CAST( YEAR( inv.[Dateofcreation] ) as varchar(4))
|
|
||||||
, [Name d. Kunden] = [customerName]
|
|
||||||
, [∑ Umsätze] = FORMAT( SUM( ISNULL(inv.[InvoiceBalance],0) ), '#,0.00 €', 'de')
|
|
||||||
, [style:∑ Umsätze] = 'text-align: right'
|
|
||||||
, [% Anteil] = CASE WHEN yr.[total] > 0 THEN FORMAT( SUM( ISNULL(inv.[InvoiceBalance],0) ) / [total], '0 %', 'de') ELSE NULL END
|
|
||||||
, [style:% Anteil] = 'text-align: right'
|
|
||||||
, [order] = ROW_NUMBER() OVER ( ORDER By SUM( ISNULL(inv.[InvoiceBalance],0) ) DESC)
|
|
||||||
FROM inv LEFT JOIN yr on YEAR( inv.[Dateofcreation] ) = yr.[year]
|
|
||||||
GROUP BY YEAR( inv.[Dateofcreation] ), yr.[total], [customerName];
|
|
||||||
|
|
||||||
END
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__r_getBalanceByYearTopMaterial]
|
|
||||||
@year int,
|
|
||||||
|
|
||||||
@authuser varchar(100)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
|
|
||||||
DECLARE @today date = GETDATE()
|
|
||||||
DECLARE @tomorrow date = DATEADD(DAY,1,@today);
|
|
||||||
|
|
||||||
IF [dbo].[fis_getModuleAuth]('fds_reports', @authuser) < 2
|
|
||||||
THROW 60000, N'not authorized', 1;
|
|
||||||
|
|
||||||
DECLARE @Admin [dbo].[fds__tt__admin_ReportAdminTable];
|
|
||||||
|
|
||||||
--Admin Table
|
|
||||||
INSERT INTO @Admin
|
|
||||||
VALUES (
|
|
||||||
'bbm'
|
|
||||||
,'Umsatz der Top 10 Materialien für Jahr ' + CAST( @year as varchar(10))
|
|
||||||
,'Umsatz der Top 10 Materialien für Jahr ' + CAST( @year as varchar(10))
|
|
||||||
,'' + FORMAT(GETDATE(), 'yyyy-MM-dd HH:mm')
|
|
||||||
,''
|
|
||||||
,'table' --typ
|
|
||||||
, '' --settings
|
|
||||||
, ''
|
|
||||||
, 0);
|
|
||||||
|
|
||||||
SELECT * FROM @Admin;
|
|
||||||
|
|
||||||
|
|
||||||
WITH inv as(
|
|
||||||
select
|
|
||||||
[itmPrice] = SUM( (ISNULL(itm.[price],0) * ISNULL(itm.[QuantityHours],0) ) -ISNULL(itm.[discount],0) )
|
|
||||||
,[Dateofcreation] = CAST(_i.[Dateofcreation] as date)
|
|
||||||
,[material] = itm.NameOrNumber
|
|
||||||
,[seq] = ROW_NUMBER() OVER (ORDER BY CAST(_i.[Dateofcreation] as date))
|
|
||||||
FROM [dbo].[mfr__invoices] as _i
|
|
||||||
JOIN [dbo].[mfr__servicerequests] as srq on srq.InvoiceId like ('%' + _i.[InvoiceId] + '%')
|
|
||||||
JOIN [dbo].[mfr__items] as itm on srq.Id = itm.[ServiceRequestId] and itm.[Type] like 'Material'
|
|
||||||
where _i.[invoicestate] in('eIsSent','eIsPaid','eIsOpen') and YEAR(_i.[Dateofcreation]) = @year
|
|
||||||
GROUP BY CAST(_i.[Dateofcreation] as date), itm.NameOrNumber
|
|
||||||
UNION
|
|
||||||
SELECT
|
|
||||||
[itmPrice] = SUM( ISNULL(itm.[value_total],0) )
|
|
||||||
,[Dateofcreation] = CAST(_i.[DateCreated] as date)
|
|
||||||
,[material] = itm.[text]
|
|
||||||
,[seq] = ROW_NUMBER() OVER (ORDER BY CAST(_i.[DateCreated] as date))
|
|
||||||
FROM [dbo].[fds__invoices] as _i
|
|
||||||
JOIN [fds__invoice_items] as itm on itm.[invid] = _i.[id] AND itm.[Type] like 'Material'
|
|
||||||
where YEAR(_i.[DateCreated]) = @year and _i.[isfinal] = 1
|
|
||||||
GROUP BY CAST(_i.[DateCreated] as date), itm.[text]
|
|
||||||
), yr1 as (
|
|
||||||
SELECT
|
|
||||||
[year] = YEAR( inv.[Dateofcreation] )
|
|
||||||
, [total] = SUM( ISNULL(inv.[InvoiceBalance],0) )
|
|
||||||
FROM [dbo].[mfr__invoices] as inv
|
|
||||||
where inv.[invoicestate] in('eIsSent','eIsPaid','eIsOpen')
|
|
||||||
GROUP BY YEAR( inv.[Dateofcreation])
|
|
||||||
UNION
|
|
||||||
SELECT
|
|
||||||
[year] = YEAR( _i.[DateCreated] )
|
|
||||||
, [total] = SUM( ISNULL(_i.[InvoiceBalance],0) )
|
|
||||||
FROM [dbo].[fds__invoices] as _i
|
|
||||||
WHERE _i.[isfinal] = 1
|
|
||||||
GROUP BY YEAR( _i.[DateCreated])
|
|
||||||
), yr as (
|
|
||||||
SELECT [year], [total] = SUM([total])
|
|
||||||
from yr1 GROUP BY [year]
|
|
||||||
)
|
|
||||||
SELECT TOP(10)
|
|
||||||
[Jahr] = CAST( YEAR( inv.[Dateofcreation] ) as varchar(4))
|
|
||||||
, [Material] = [material]
|
|
||||||
, [∑ Umsätze] = FORMAT( SUM( ISNULL(inv.[itmPrice],0) ), '#,0.00 €', 'de')
|
|
||||||
, [style:∑ Umsätze] = 'text-align: right'
|
|
||||||
, [% Anteil] = CASE WHEN yr.[total] > 0 THEN FORMAT( SUM( ISNULL(inv.[itmPrice],0) ) / [total], '0 %', 'de') ELSE NULL END
|
|
||||||
, [style:% Anteil] = 'text-align: right'
|
|
||||||
, [order] = ROW_NUMBER() OVER ( ORDER By SUM( ISNULL(inv.[itmPrice],0) ) DESC)
|
|
||||||
FROM inv LEFT JOIN yr on YEAR( inv.[Dateofcreation] ) = yr.[year]
|
|
||||||
GROUP BY YEAR( inv.[Dateofcreation] ), yr.[total], [material];
|
|
||||||
|
|
||||||
END
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__r_getBalanceTrendByMonth]
|
|
||||||
@year int
|
|
||||||
,@month int,
|
|
||||||
|
|
||||||
@authuser varchar(100)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @today date = GETDATE()
|
|
||||||
DECLARE @tomorrow date = DATEADD(DAY,1,@today);
|
|
||||||
|
|
||||||
IF [dbo].[fis_getModuleAuth]('fds_reports', @authuser) < 2
|
|
||||||
THROW 60000, N'not authorized', 1;
|
|
||||||
|
|
||||||
set @month = ISNULL(@month, Month(@today));
|
|
||||||
set @year= ISNULL(@year, Year(@year));
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @Admin [dbo].[fds__tt__admin_ReportAdminTable];
|
|
||||||
|
|
||||||
--Admin Table
|
|
||||||
INSERT INTO @Admin
|
|
||||||
VALUES (
|
|
||||||
'bbm'
|
|
||||||
,'UmsatzTrend für Monat ' + FORMAT(@Month, '00') + ' / ' + FORMAT(@year, '0000')
|
|
||||||
,'UmsatzTrend für Monat ' + FORMAT(@Month, '00') + ' / ' + FORMAT(@year, '0000')
|
|
||||||
,'' + FORMAT(GETDATE(), 'yyyy-MM-dd HH:mm')
|
|
||||||
,''
|
|
||||||
,'chart' --typ
|
|
||||||
, '{ "charttype": "line", "x1_label_angle": -90, "color": "e0301e", "linewidth": 2, "valuelabels": false, "marker": "none", "colorpalette": "00B050;00B050;00B050", "x1_column": "date", "y1_column": "Value", "y1_title": "Actuals", "x1_title": "", "series_column": "series", "legend_position": "bottom", "show_datalabel": true, "datalabelfontsizescale": 0.6, "height": 600, "width": 1200, "x1_labelformat": "ddd, dd/MM"}' --settings
|
|
||||||
, ''
|
|
||||||
, 0)
|
|
||||||
|
|
||||||
SELECT * FROM @Admin;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
with inv as(
|
|
||||||
select
|
|
||||||
[InvoiceBalance] = SUM( ISNULL(_i.[InvoiceBalance],0) )
|
|
||||||
,[Dateofcreation] = CAST([Dateofcreation] as date)
|
|
||||||
,[seq] = ROW_NUMBER() OVER (ORDER BY CAST([Dateofcreation] as date))
|
|
||||||
FROM [dbo].[mfr__invoices] as _i
|
|
||||||
where _i.[invoicestate] in('eIsSent','eIsPaid','eIsOpen') and month([Dateofcreation]) = @Month and YEAR([Dateofcreation]) = @year
|
|
||||||
GROUP BY CAST([Dateofcreation] as date)
|
|
||||||
UNION
|
|
||||||
select
|
|
||||||
[InvoiceBalance] = SUM( ISNULL(_i.[InvoiceBalance],0) )
|
|
||||||
,[Dateofcreation] = CAST([Datecreated] as date)
|
|
||||||
,[seq] = ROW_NUMBER() OVER (ORDER BY CAST([Datecreated] as date))
|
|
||||||
FROM [dbo].[fds__invoices] as _i where _i.IsFinal = 1 and month([Datecreated]) = @Month and YEAR([Datecreated]) = @year
|
|
||||||
GROUP BY CAST([Datecreated] as date)
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
--[Monat] = FORMAT( inv.[Dateofcreation], 'yyyy - MM', 'de')
|
|
||||||
--,[Umsatz] = FORMAT( ISNULL(inv.[InvoiceBalance],0) , '#0.00', 'de')
|
|
||||||
[series] = 'Umsatz'
|
|
||||||
,[date] = CAST(inv.[Dateofcreation] as date)
|
|
||||||
,[value] = CAST( SUM( ISNULL(inv.[InvoiceBalance],0) ) OVER (ORDER BY [seq]) as numeric(9,2))
|
|
||||||
FROM inv
|
|
||||||
ORDER BY [seq]
|
|
||||||
|
|
||||||
END
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
|
|
||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__r_getBalanceTrendByYear]
|
|
||||||
@year int,
|
|
||||||
|
|
||||||
@authuser varchar(100)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @today date = GETDATE()
|
|
||||||
DECLARE @tomorrow date = DATEADD(DAY,1,@today);
|
|
||||||
|
|
||||||
IF [dbo].[fis_getModuleAuth]('fds_reports', @authuser) < 2
|
|
||||||
THROW 60000, N'not authorized', 1;
|
|
||||||
|
|
||||||
set @year= ISNULL(@year, Year(@year));
|
|
||||||
|
|
||||||
|
|
||||||
DECLARE @Admin [dbo].[fds__tt__admin_ReportAdminTable];
|
|
||||||
|
|
||||||
--Admin Table
|
|
||||||
INSERT INTO @Admin
|
|
||||||
VALUES (
|
|
||||||
'bbm'
|
|
||||||
,'UmsatzTrend für Jahr ' + FORMAT(@year, '0000')
|
|
||||||
,'UmsatzTrend für Jahr ' + FORMAT(@year, '0000')
|
|
||||||
,'' + FORMAT(GETDATE(), 'yyyy-MM-dd HH:mm')
|
|
||||||
,''
|
|
||||||
,'chart' --typ
|
|
||||||
, '{ "charttype": "line", "x1_label_angle": -90, "color": "e0301e", "linewidth": 2, "valuelabels": false, "marker": "none", "colorpalette": "00B050;00B050;00B050", "x1_column": "date", "y1_column": "Value", "y1_title": "Actuals", "x1_title": "", "series_column": "series", "legend_position": "bottom", "show_datalabel": true, "datalabelfontsizescale": 0.6, "height": 600, "width": 1200, "x1_labelformat": "ddd, dd/MM"}' --settings
|
|
||||||
, ''
|
|
||||||
, 0)
|
|
||||||
|
|
||||||
SELECT * FROM @Admin;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
with inv as(
|
|
||||||
select
|
|
||||||
[InvoiceBalance] = SUM( ISNULL(_i.[InvoiceBalance],0) )
|
|
||||||
,[Dateofcreation] = CAST([Dateofcreation] as date)
|
|
||||||
,[seq] = ROW_NUMBER() OVER (ORDER BY CAST([Dateofcreation] as date))
|
|
||||||
FROM [dbo].[mfr__invoices] as _i
|
|
||||||
where _i.[invoicestate] in('eIsSent','eIsPaid','eIsOpen') and YEAR([Dateofcreation]) = @year
|
|
||||||
GROUP BY CAST([Dateofcreation] as date)
|
|
||||||
UNION
|
|
||||||
select
|
|
||||||
[InvoiceBalance] = SUM( ISNULL(_i.[InvoiceBalance],0) )
|
|
||||||
,[Dateofcreation] = CAST([Datecreated] as date)
|
|
||||||
,[seq] = ROW_NUMBER() OVER (ORDER BY CAST([Datecreated] as date))
|
|
||||||
FROM [dbo].[fds__invoices] as _i where _i.IsFinal = 1 and YEAR([Datecreated]) = @year
|
|
||||||
GROUP BY CAST([Datecreated] as date)
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
--[Monat] = FORMAT( inv.[Dateofcreation], 'yyyy - MM', 'de')
|
|
||||||
--,[Umsatz] = FORMAT( ISNULL(inv.[InvoiceBalance],0) , '#0.00', 'de')
|
|
||||||
[series] = 'Umsatz'
|
|
||||||
,[date] = CAST(inv.[Dateofcreation] as date)
|
|
||||||
,[value] = CAST( SUM( ISNULL(inv.[InvoiceBalance],0) ) OVER (ORDER BY [seq]) as numeric(9,2))
|
|
||||||
FROM inv
|
|
||||||
ORDER BY [seq]
|
|
||||||
|
|
||||||
END
|
|
||||||
-70
@@ -1,70 +0,0 @@
|
|||||||
|
|
||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__setBankingtransaction_autoAssigns]
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info])
|
|
||||||
VALUES ('fds__setBankingtransaction_autoAssigns' ,'', '');
|
|
||||||
|
|
||||||
with
|
|
||||||
icr as (
|
|
||||||
SELECT * FROM [dbo].[fds__getInvoiceCredits] (null, null)
|
|
||||||
), ic as (
|
|
||||||
SELECT
|
|
||||||
icr.[uid]
|
|
||||||
, icr.[InvoiceId]
|
|
||||||
, [InvID] = CAST(i.[Id] as varchar(15))
|
|
||||||
, icr.amount
|
|
||||||
, [auto] = CAST(1 as bit)
|
|
||||||
FROM icr
|
|
||||||
JOIN [dbo].[fds__invoices] as i on icr.[InvoiceId] = i.[InvoiceId] and i.[InvoiceId] is not null and i.[IsFinal] = 1
|
|
||||||
UNION
|
|
||||||
SELECT
|
|
||||||
icr.[uid]
|
|
||||||
, icr.[InvoiceId]
|
|
||||||
, [InvID] = CAST(i.[Id] as varchar(15))
|
|
||||||
, icr.amount
|
|
||||||
, [auto] = CAST(1 as bit)
|
|
||||||
FROM icr
|
|
||||||
JOIN [dbo].[mfr__invoices] as i on icr.[InvoiceId] = i.[InvoiceId] and i.[InvoiceId] is not null
|
|
||||||
--UNION
|
|
||||||
--SELECT
|
|
||||||
-- bt.[uid]
|
|
||||||
-- , i.[InvoiceId]
|
|
||||||
-- , [InvID] = i.[Id]
|
|
||||||
-- , bt.[amount]
|
|
||||||
-- , [auto] = CAST(0 as bit)
|
|
||||||
--FROM [dbo].[fds__bankingtransactions] as bt JOIN [dbo].[fds__bankingtransactions_settings] as bs ON bt.uid = bs.[banking_uid] AND ISNULL(bs.[assigned_invoice_id], '') <> ''
|
|
||||||
-- JOIN [dbo].[fds__invoices] as i on bs.[assigned_invoice_id] = i.[Id] and i.[IsFinal] = 1
|
|
||||||
|
|
||||||
), ic2 as (
|
|
||||||
SELECT [banking_uid] = ic.[uid]
|
|
||||||
, [amount] = SUM(ISNULL(ic.[amount], 0.0))
|
|
||||||
, [InvID_csv] = STRING_AGG(ic.[InvID], ',')
|
|
||||||
from ic
|
|
||||||
WHERE ic.[InvID] is not null
|
|
||||||
GROUP BY ic.[uid]
|
|
||||||
)
|
|
||||||
--select * from ic2
|
|
||||||
MERGE [dbo].[fds__bankingtransactions_settings] as TARGET
|
|
||||||
USING ic2 as SOURCE ON TARGET.[banking_uid] = SOURCE.[banking_uid]
|
|
||||||
WHEN NOT MATCHED BY TARGET THEN
|
|
||||||
INSERT
|
|
||||||
([banking_uid]
|
|
||||||
,[auto_invoice_id])
|
|
||||||
VALUES
|
|
||||||
(SOURCE.[banking_uid]
|
|
||||||
,SOURCE.[InvID_csv])
|
|
||||||
WHEN MATCHED THEN
|
|
||||||
UPDATE SET [auto_invoice_id] = [dbo].[ott_merge_csv](TARGET.[auto_invoice_id],SOURCE.[InvID_csv]);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
END
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
-- =============================================
|
|
||||||
-- Author: <Author,,Name>
|
|
||||||
-- Create date: <Create Date,,>
|
|
||||||
-- Description: <Description,,>
|
|
||||||
-- =============================================
|
|
||||||
CREATE PROCEDURE [dbo].[backup__fds__setInvoicePayed]
|
|
||||||
@Id varchar(15),
|
|
||||||
@authuser varchar(25)
|
|
||||||
AS
|
|
||||||
BEGIN
|
|
||||||
SET NOCOUNT ON;
|
|
||||||
DECLARE @now datetime = GETUTCDATE();
|
|
||||||
|
|
||||||
IF [dbo].[fis_getModuleAuth]('fds_inv', @authuser) < 2
|
|
||||||
THROW 60000, N'not authorized', 1;
|
|
||||||
|
|
||||||
INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info])
|
|
||||||
VALUES ('fds__setInvoicePayed' ,@authuser , (SELECT * FROM (VALUES(@Id, @authuser)) as z ([id],[authuser]) FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER));
|
|
||||||
|
|
||||||
UPDATE [dbo].[fds__invoices] SET [IsPayed] = 1, [paymentstatus] = 'm', [DateModified] = @now, [UserModified] = @authuser, [version] = (ISNULL([version],0) + 1)
|
|
||||||
OUTPUT inserted.*
|
|
||||||
WHERE [Id] = @Id AND @Id is not null and [IsPayed] = 0;
|
|
||||||
|
|
||||||
IF TRY_CAST(@id as bigint) is not null and EXISTS(SELECT 0 FROM [dbo].[mfr__invoices] as i where i.[id] = TRY_CAST(@id as bigint))
|
|
||||||
BEGIN
|
|
||||||
MERGE [dbo].[fds__custom_invoiceinfo] as TARGET
|
|
||||||
USING (VALUES
|
|
||||||
(TRY_CAST(@id as bigint)
|
|
||||||
,CAST('m' as char(1))
|
|
||||||
,CAST(1 as bit))) as SOURCE([InvId],[PaymentStatus],[isPayed])
|
|
||||||
ON TARGET.[InvID] = SOURCE.[InvID]
|
|
||||||
WHEN MATCHED THEN
|
|
||||||
UPDATE SET TARGET.[PaymentStatus] = SOURCE.[PaymentStatus]
|
|
||||||
,TARGET.[isPayed] = SOURCE.[isPayed]
|
|
||||||
WHEN NOT MATCHED THEN
|
|
||||||
INSERT ([InvId],[PaymentStatus],[isPayed],[file])
|
|
||||||
VALUES(
|
|
||||||
SOURCE.[InvId]
|
|
||||||
,SOURCE.[PaymentStatus]
|
|
||||||
,SOURCE.[isPayed]
|
|
||||||
,NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
END
|
|
||||||
|
|
||||||
END
|
|
||||||
@@ -209,14 +209,12 @@ BEGIN
|
|||||||
itm.*
|
itm.*
|
||||||
, [baseorder] = s.[baseorder]
|
, [baseorder] = s.[baseorder]
|
||||||
, [order] = ROW_NUMBER() OVER (PARTITION BY itm.[ServiceRequestId] ORDER BY itm.[SortOrder], itm.[Id])
|
, [order] = ROW_NUMBER() OVER (PARTITION BY itm.[ServiceRequestId] ORDER BY itm.[SortOrder], itm.[Id])
|
||||||
, [SetItmID] = CASE WHEN itm.[Type] = 'Set' AND ISNULL(itm.[Price], 0) > 0 THEN NULL
|
, [SetItmID] = MAX(CASE WHEN itm.[Type] = 'Set' AND ISNULL(itm.[Price], 0) = 0 THEN itm.[Id] ELSE NULL END)
|
||||||
ELSE MAX(CASE WHEN itm.[Type] = 'Set' AND ISNULL(itm.[Price], 0) > 0 THEN itm.[Id] ELSE NULL END)
|
|
||||||
OVER (
|
OVER (
|
||||||
PARTITION BY itm.[ServiceRequestId]
|
PARTITION BY itm.[ServiceRequestId]
|
||||||
ORDER BY itm.[SortOrder], itm.[Id]
|
ORDER BY itm.[SortOrder], itm.[Id]
|
||||||
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
||||||
)
|
)
|
||||||
END
|
|
||||||
FROM [dbo].[mfr__items] as itm
|
FROM [dbo].[mfr__items] as itm
|
||||||
JOIN @SReq as s ON itm.ServiceRequestId = s.[Id]
|
JOIN @SReq as s ON itm.ServiceRequestId = s.[Id]
|
||||||
WHERE itm.[Type] <> 'PartialInvoice';
|
WHERE itm.[Type] <> 'PartialInvoice';
|
||||||
|
|||||||
@@ -207,14 +207,12 @@ BEGIN
|
|||||||
SELECT itm.*
|
SELECT itm.*
|
||||||
, [baseorder] = s.[baseorder]
|
, [baseorder] = s.[baseorder]
|
||||||
, [order] = ROW_NUMBER() OVER (PARTITION BY itm.[ServiceRequestId] ORDER BY itm.[SortOrder], itm.[Id])
|
, [order] = ROW_NUMBER() OVER (PARTITION BY itm.[ServiceRequestId] ORDER BY itm.[SortOrder], itm.[Id])
|
||||||
, [SetItmID] = CASE WHEN itm.[Type] = 'Set' AND ISNULL(itm.[Price], 0) > 0 THEN NULL
|
, [SetItmID] = MAX(CASE WHEN itm.[Type] = 'Set' AND ISNULL(itm.[Price], 0) = 0 THEN itm.[Id] ELSE NULL END)
|
||||||
ELSE MAX(CASE WHEN itm.[Type] = 'Set' AND ISNULL(itm.[Price], 0) > 0 THEN itm.[Id] ELSE NULL END)
|
|
||||||
OVER (
|
OVER (
|
||||||
PARTITION BY itm.[ServiceRequestId]
|
PARTITION BY itm.[ServiceRequestId]
|
||||||
ORDER BY itm.[SortOrder], itm.[Id]
|
ORDER BY itm.[SortOrder], itm.[Id]
|
||||||
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
||||||
)
|
)
|
||||||
END
|
|
||||||
FROM [dbo].[mfr__items] as itm
|
FROM [dbo].[mfr__items] as itm
|
||||||
JOIN @SReq as s ON itm.ServiceRequestId = s.[Id];
|
JOIN @SReq as s ON itm.ServiceRequestId = s.[Id];
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user