diff --git a/src/SimpleAccounting/Abstractions/DotNetProcess.cs b/src/SimpleAccounting/Abstractions/DotNetProcess.cs index 208c9937..fc1020e3 100644 --- a/src/SimpleAccounting/Abstractions/DotNetProcess.cs +++ b/src/SimpleAccounting/Abstractions/DotNetProcess.cs @@ -17,7 +17,7 @@ namespace lg2de.SimpleAccounting.Abstractions; [ExcludeFromCodeCoverage( Justification = "The abstraction is for unit testing only. This is the simple implementation.")] [UsedImplicitly] -internal class DotNetProcess : IProcess +internal partial class DotNetProcess : IProcess { public Process? GetProcessByName(string processName) { @@ -74,7 +74,7 @@ public void ShellExecute(string fileName) [ExcludeFromCodeCoverage(Justification = "The WinAPI cannot be tested.")] [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Names are following external API.")] - private static class WinApi + private static partial class WinApi { private const int SW_MINIMIZE = 6; private const int SW_RESTORE = 9; @@ -96,13 +96,16 @@ public static void MinimizeProcess(Process process) ShowWindow(handle, SW_MINIMIZE); } - [DllImport("User32.dll")] - private static extern bool SetForegroundWindow(IntPtr handle); + [LibraryImport("User32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool SetForegroundWindow(IntPtr handle); - [DllImport("User32.dll")] - private static extern bool ShowWindow(IntPtr handle, int nCmdShow); + [LibraryImport("User32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool ShowWindow(IntPtr handle, int nCmdShow); - [DllImport("User32.dll")] - private static extern bool IsIconic(IntPtr handle); + [LibraryImport("User32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool IsIconic(IntPtr handle); } } diff --git a/src/SimpleAccounting/Abstractions/SystemClock.cs b/src/SimpleAccounting/Abstractions/SystemClock.cs index 8de79cb4..8563194d 100644 --- a/src/SimpleAccounting/Abstractions/SystemClock.cs +++ b/src/SimpleAccounting/Abstractions/SystemClock.cs @@ -13,6 +13,12 @@ namespace lg2de.SimpleAccounting.Abstractions; [SuppressMessage( "Major Code Smell", "S6354:Use a testable date/time provider", Justification = "This is the one and only implementation according pattern.")] +[SuppressMessage( + "Major Code Smell", "S6563:Use UTC when recording DateTime instants", + Justification = "The application works with local calendar dates.")] +[SuppressMessage( + "Major Code Smell", "S6566:Use DateTimeOffset instead of DateTime", + Justification = "The application works with local calendar dates.")] internal class SystemClock : IClock { public DateTime Now() diff --git a/src/SimpleAccounting/Extensions/DateTimeExtensions.cs b/src/SimpleAccounting/Extensions/DateTimeExtensions.cs index 5ab63072..3565c0f0 100644 --- a/src/SimpleAccounting/Extensions/DateTimeExtensions.cs +++ b/src/SimpleAccounting/Extensions/DateTimeExtensions.cs @@ -17,17 +17,22 @@ internal static class DateTimeExtensions public static DateTime ToDateTime(this uint date) { // converts from date format yyyymmdd (as uint) into Date(Time) instance - return new DateTime( - (int)date / YearFactor, - (int)(date / MonthFactor) % MonthFactor, - (int)date % MonthFactor, - 0, 0, 0, - DateTimeKind.Local); + return new DateOnly( + (int)date / YearFactor, + (int)(date / MonthFactor) % MonthFactor, + (int)date % MonthFactor) + .ToDateTime(TimeOnly.MinValue, DateTimeKind.Local); } public static uint ToAccountingDate(this DateTime date) { // converts from Date(Time) instance into date formatted as yyyymmdd (uint) + return DateOnly.FromDateTime(date).ToAccountingDate(); + } + + public static uint ToAccountingDate(this DateOnly date) + { + // converts from Date instance into date formatted as yyyymmdd (uint) return (uint)(date.Year * YearFactor + date.Month * MonthFactor + date.Day); } } diff --git a/src/SimpleAccounting/Model/AccountingData.Extensions.cs b/src/SimpleAccounting/Model/AccountingData.Extensions.cs index 4443bbfd..77eeb333 100644 --- a/src/SimpleAccounting/Model/AccountingData.Extensions.cs +++ b/src/SimpleAccounting/Model/AccountingData.Extensions.cs @@ -351,9 +351,8 @@ public static AccountingDataJournal GetOrCreateLatest(this IList()) + foreach (var type in Enum.GetValues()) { var localizedType = Resources.ResourceManager.GetString($"AccountType_{type}") ?? $"<{type}>"; Types[type] = localizedType; diff --git a/src/SimpleAccounting/Presentation/AccountsViewModel.cs b/src/SimpleAccounting/Presentation/AccountsViewModel.cs index bfdc0a6c..62bc8551 100644 --- a/src/SimpleAccounting/Presentation/AccountsViewModel.cs +++ b/src/SimpleAccounting/Presentation/AccountsViewModel.cs @@ -24,8 +24,6 @@ internal class AccountsViewModel : Screen, IAccountsViewModel private readonly IProjectData projectData; private readonly IWindowManager windowManager; - private AccountViewModel? selectedAccount; - public AccountsViewModel(IWindowManager windowManager, IProjectData projectData) { this.windowManager = windowManager; @@ -36,10 +34,10 @@ public AccountsViewModel(IWindowManager windowManager, IProjectData projectData) public AccountViewModel? SelectedAccount { - get => this.selectedAccount; + get; set { - this.selectedAccount = value; + field = value; this.NotifyOfPropertyChange(); } } @@ -206,6 +204,72 @@ public Task OnEditAccountAsync(object? commandParameter) return this.ShowEditAccountDialogAsync(account); } + private static void SaveImportConfiguration(AccountViewModel viewModel, AccountDefinition accountDefinition) + { + if (!viewModel.IsImportActive) + { + accountDefinition.ImportMapping = null; + return; + } + + // save required items + accountDefinition.ImportMapping = new AccountDefinitionImportMapping + { + Columns = + [ + new AccountDefinitionImportMappingColumnsColumn + { + Target = AccountDefinitionImportMappingColumnsColumnTarget.Date, + Source = viewModel.ImportDateSource, + IgnorePattern = viewModel.ImportDateIgnorePattern + }, + new AccountDefinitionImportMappingColumnsColumn + { + Target = AccountDefinitionImportMappingColumnsColumnTarget.Value, + Source = viewModel.ImportValueSource, + IgnorePattern = viewModel.ImportValueIgnorePattern + } + ] + }; + + // save optional items + if (!string.IsNullOrWhiteSpace(viewModel.ImportTextSource)) + { + accountDefinition.ImportMapping.Columns.Add( + new AccountDefinitionImportMappingColumnsColumn + { + Target = AccountDefinitionImportMappingColumnsColumnTarget.Text, + Source = viewModel.ImportTextSource, + IgnorePattern = viewModel.ImportTextIgnorePattern + }); + } + + if (!string.IsNullOrWhiteSpace(viewModel.ImportNameSource)) + { + accountDefinition.ImportMapping.Columns.Add( + new AccountDefinitionImportMappingColumnsColumn + { + Target = AccountDefinitionImportMappingColumnsColumnTarget.Name, + Source = viewModel.ImportNameSource, + IgnorePattern = viewModel.ImportNameIgnorePattern + }); + } + + if (!viewModel.ImportPatterns.Any()) + { + return; + } + + accountDefinition.ImportMapping.Patterns = viewModel.ImportPatterns.Select(x => + new AccountDefinitionImportMappingPatternsPattern + { + Expression = x.Expression, + Value = x.Value?.ToModelValue() ?? 0, + ValueSpecified = x.Value.HasValue, + AccountID = x.Account?.ID ?? 0 + }).ToList(); + } + private async Task ShowEditAccountDialogAsync(AccountViewModel selectedAccountViewModel) { // clone view model for easy rollback @@ -263,11 +327,10 @@ private async Task ShowEditAccountDialogAsync(AccountViewModel selectedAccountVi clonedViewModel.Group!.Account.Add(accountData); // reorder the accounts in the new group - clonedViewModel.Group!.Account = - clonedViewModel.Group.Account.OrderBy(x => x.ID).ToList(); + clonedViewModel.Group.Account = clonedViewModel.Group.Account.OrderBy(x => x.ID).ToList(); } - this.SaveImportConfiguration(clonedViewModel, accountData); + SaveImportConfiguration(clonedViewModel, accountData); // update view model selectedAccountViewModel.Name = clonedViewModel.Name; @@ -339,72 +402,6 @@ private void UpdateImportCandidateAccounts(AccountViewModel accountViewModel) } } - private void SaveImportConfiguration(AccountViewModel viewModel, AccountDefinition accountDefinition) - { - if (!viewModel.IsImportActive) - { - accountDefinition.ImportMapping = null; - return; - } - - // save required items - accountDefinition.ImportMapping = new AccountDefinitionImportMapping - { - Columns = - [ - new AccountDefinitionImportMappingColumnsColumn - { - Target = AccountDefinitionImportMappingColumnsColumnTarget.Date, - Source = viewModel.ImportDateSource, - IgnorePattern = viewModel.ImportDateIgnorePattern - }, - new AccountDefinitionImportMappingColumnsColumn - { - Target = AccountDefinitionImportMappingColumnsColumnTarget.Value, - Source = viewModel.ImportValueSource, - IgnorePattern = viewModel.ImportValueIgnorePattern - } - ] - }; - - // save optional items - if (!string.IsNullOrWhiteSpace(viewModel.ImportTextSource)) - { - accountDefinition.ImportMapping.Columns.Add( - new AccountDefinitionImportMappingColumnsColumn - { - Target = AccountDefinitionImportMappingColumnsColumnTarget.Text, - Source = viewModel.ImportTextSource, - IgnorePattern = viewModel.ImportTextIgnorePattern - }); - } - - if (!string.IsNullOrWhiteSpace(viewModel.ImportNameSource)) - { - accountDefinition.ImportMapping.Columns.Add( - new AccountDefinitionImportMappingColumnsColumn - { - Target = AccountDefinitionImportMappingColumnsColumnTarget.Name, - Source = viewModel.ImportNameSource, - IgnorePattern = viewModel.ImportNameIgnorePattern - }); - } - - if (!viewModel.ImportPatterns.Any()) - { - return; - } - - accountDefinition.ImportMapping.Patterns = viewModel.ImportPatterns.Select(x => - new AccountDefinitionImportMappingPatternsPattern - { - Expression = x.Expression, - Value = x.Value?.ToModelValue() ?? 0, - ValueSpecified = x.Value.HasValue, - AccountID = x.Account?.ID ?? 0 - }).ToList(); - } - private void RefreshAccountList() { IEnumerable accounts = this.allAccounts; diff --git a/src/SimpleAccounting/Presentation/EditBookingDesignViewModel.cs b/src/SimpleAccounting/Presentation/EditBookingDesignViewModel.cs index 5b199c54..38c16c7c 100644 --- a/src/SimpleAccounting/Presentation/EditBookingDesignViewModel.cs +++ b/src/SimpleAccounting/Presentation/EditBookingDesignViewModel.cs @@ -4,7 +4,6 @@ namespace lg2de.SimpleAccounting.Presentation; -using System; using System.Diagnostics.CodeAnalysis; using lg2de.SimpleAccounting.Abstractions; using lg2de.SimpleAccounting.Model; @@ -19,15 +18,13 @@ namespace lg2de.SimpleAccounting.Presentation; [SuppressMessage( "Major Code Smell", "S4055:Literals should not be passed as localized parameters", Justification = "Design view model defines useful values")] -[SuppressMessage( - "Major Code Smell", - "S6354:Use a testable date/time provider", - Justification = "Is ok for design view model.")] internal class EditBookingDesignViewModel : EditBookingViewModel { + private static readonly SystemClock Clock = new(); + public EditBookingDesignViewModel() : base( - new ProjectData(new Settings(), null!, null!, null!, new SystemClock(), null!), DateTime.Now, + new ProjectData(new Settings(), null!, null!, null!, Clock, null!), Clock.Now(), editMode: false) { this.BookingIdentifier = 42; diff --git a/src/SimpleAccounting/Presentation/ImportBookingsDesignViewModel.cs b/src/SimpleAccounting/Presentation/ImportBookingsDesignViewModel.cs index 7544020a..45b3a1b8 100644 --- a/src/SimpleAccounting/Presentation/ImportBookingsDesignViewModel.cs +++ b/src/SimpleAccounting/Presentation/ImportBookingsDesignViewModel.cs @@ -22,12 +22,10 @@ namespace lg2de.SimpleAccounting.Presentation; [SuppressMessage( "Major Code Smell", "S4055:Literals should not be passed as localized parameters", Justification = "Design view model defines useful values")] -[SuppressMessage( - "Major Code Smell", - "S6354:Use a testable date/time provider", - Justification = "Is ok for design view model.")] internal class ImportBookingsDesignViewModel : ImportBookingsViewModel { + private static readonly SystemClock Clock = new(); + // define some accounts private static readonly List SampleAccounts = [ @@ -61,13 +59,13 @@ internal class ImportBookingsDesignViewModel : ImportBookingsViewModel [ new AccountingDataJournal { - DateStart = (uint)(DateTime.Today.Year * 10000 + 101), - DateEnd = (uint)(DateTime.Today.Year * 10000 + 1231), + DateStart = (uint)(Clock.Now().Year * 10000 + 101), + DateEnd = (uint)(Clock.Now().Year * 10000 + 1231), Booking = [ new AccountingDataJournalBooking { - Date = (uint)(DateTime.Today.Year * 10000 + 1231), + Date = (uint)(Clock.Now().Year * 10000 + 1231), ID = 999, Credit = [new BookingValue { Account = 100, Text = "End of year", Value = 1234 }], Debit = [new BookingValue { Account = 990, Text = "End of year", Value = 1234 }] @@ -79,16 +77,16 @@ internal class ImportBookingsDesignViewModel : ImportBookingsViewModel }; public ImportBookingsDesignViewModel() - : base(null!, null!, new ProjectData(new Settings(), null!, null!, null!, new SystemClock(), null!)) + : base(null!, null!, new ProjectData(new Settings(), null!, null!, null!, Clock, null!)) { this.ProjectData.LoadData(SampleData); this.SelectedAccountNumber = 100; - this.StartDate = DateTime.Today; + this.StartDate = Clock.Now().Date; this.LoadedData.Add( new ImportEntryViewModel(SampleAccounts) { - Date = DateTime.Now - TimeSpan.FromDays(1), + Date = Clock.Now() - TimeSpan.FromDays(1), Name = "Should not be visible!", Text = "Should not be visible!", RemoteAccount = SampleAccounts.Single(x => x.ID == 600), @@ -97,7 +95,7 @@ public ImportBookingsDesignViewModel() this.LoadedData.Add( new ImportEntryViewModel(SampleAccounts) { - Date = DateTime.Now, + Date = Clock.Now(), Name = "McX", Text = "Shoes", RemoteAccount = SampleAccounts.Single(x => x.ID == 600), @@ -107,7 +105,7 @@ public ImportBookingsDesignViewModel() this.LoadedData.Add( new ImportEntryViewModel(SampleAccounts) { - Date = DateTime.Now + TimeSpan.FromDays(1), + Date = Clock.Now() + TimeSpan.FromDays(1), Name = "McY", Text = "More Shoes", Value = 159.95, diff --git a/src/SimpleAccounting/Presentation/ShellDesignViewModel.cs b/src/SimpleAccounting/Presentation/ShellDesignViewModel.cs index 37113563..c2e439f3 100644 --- a/src/SimpleAccounting/Presentation/ShellDesignViewModel.cs +++ b/src/SimpleAccounting/Presentation/ShellDesignViewModel.cs @@ -4,7 +4,6 @@ namespace lg2de.SimpleAccounting.Presentation; -using System; using System.Diagnostics.CodeAnalysis; using lg2de.SimpleAccounting.Abstractions; using lg2de.SimpleAccounting.Extensions; @@ -23,7 +22,7 @@ namespace lg2de.SimpleAccounting.Presentation; [SuppressMessage("ReSharper", "StringLiteralTypo")] internal class ShellDesignViewModel : ShellViewModel { - private static readonly IClock Clock = new SystemClock(); + private static readonly SystemClock Clock = new(); private static readonly ProjectData DesignProject = new(new Settings(), null!, null!, null!, Clock, null!); public ShellDesignViewModel() @@ -58,17 +57,13 @@ private void LoadAccounts() this.Accounts.OnDataLoaded(); } - [SuppressMessage( - "Major Code Smell", - "S6354:Use a testable date/time provider", - Justification = "Ok for testing code")] private void LoadJournal() { int index = 0; var journalItem = new FullJournalItemViewModel(index++) { Identifier = 41, - Date = DateTime.Now, + Date = Clock.Now(), Text = "Booking 1", Value = 123.4, CreditAccount = "100", @@ -78,7 +73,7 @@ private void LoadJournal() journalItem = new FullJournalItemViewModel(index++) { Identifier = 42, - Date = DateTime.Now, + Date = Clock.Now(), Text = "Booking 2", Value = 1.444, CreditAccount = "101", @@ -89,7 +84,7 @@ private void LoadJournal() journalItem = new FullJournalItemViewModel(index++) { Identifier = 43, - Date = DateTime.Now, + Date = Clock.Now(), Text = "Booking 3", Value = 99, CreditAccount = "101", @@ -99,7 +94,7 @@ private void LoadJournal() journalItem = new FullJournalItemViewModel(index) { Identifier = 44, - Date = DateTime.Now, + Date = Clock.Now(), Text = "Booking 4", Value = -99, CreditAccount = "101", @@ -112,7 +107,7 @@ private void LoadJournal() var accountJournalItem = new AccountJournalItemViewModel(index++) { Identifier = 42, - Date = DateTime.Now, + Date = Clock.Now(), Text = "Booking 2", CreditValue = 123.4, RemoteAccount = "Div.", @@ -122,7 +117,7 @@ private void LoadJournal() accountJournalItem = new AccountJournalItemViewModel(index++) { Identifier = 44, - Date = DateTime.Now, + Date = Clock.Now(), Text = "Booking 4", CreditValue = 1.222, RemoteAccount = "Div." diff --git a/src/SimpleAccounting/SimpleAccounting.csproj b/src/SimpleAccounting/SimpleAccounting.csproj index 01768ec8..74fe225d 100644 --- a/src/SimpleAccounting/SimpleAccounting.csproj +++ b/src/SimpleAccounting/SimpleAccounting.csproj @@ -12,6 +12,8 @@ DEVEL App.ico true + + true diff --git a/tests/SimpleAccounting.UnitTests/Presentation/EditBookingViewModelTests.cs b/tests/SimpleAccounting.UnitTests/Presentation/EditBookingViewModelTests.cs index 0aa21f7b..2ce3749b 100644 --- a/tests/SimpleAccounting.UnitTests/Presentation/EditBookingViewModelTests.cs +++ b/tests/SimpleAccounting.UnitTests/Presentation/EditBookingViewModelTests.cs @@ -505,7 +505,7 @@ public void OnInitialize_AllAccountTypesAdded_AccountRelatedPropertiesNotEmpty() var processApi = Substitute.For(); var projectData = new ProjectData(new Settings(), windowManager, dialogs, fileSystem, clock, processApi); var sut = new EditBookingViewModel(projectData, YearBegin, editMode: false); - foreach (AccountDefinitionType type in Enum.GetValues(typeof(AccountDefinitionType))) + foreach (AccountDefinitionType type in Enum.GetValues()) { sut.Accounts.Add(new AccountDefinition { Name = type.ToString(), Type = type }); } diff --git a/tests/SimpleAccounting.UnitTests/Presentation/Samples.cs b/tests/SimpleAccounting.UnitTests/Presentation/Samples.cs index dd03a594..4291f33d 100644 --- a/tests/SimpleAccounting.UnitTests/Presentation/Samples.cs +++ b/tests/SimpleAccounting.UnitTests/Presentation/Samples.cs @@ -126,7 +126,7 @@ public static AccountingData SampleProject ] }; accountingData.Accounts[^1].Account.AddRange( - Enum.GetValues(typeof(AccountDefinitionType)).Cast().Select( + Enum.GetValues().Select( type => new AccountDefinition { ID = (ulong)(9000 + type), Name = $"Active empty {type}", Type = type, Active = true