diff --git a/.vscode/launch.json b/.vscode/launch.json index 2896279..bea5007 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,11 +4,22 @@ // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md "version": "0.2.0", "configurations": [ - - - { - "name": ".NET Core Launch (console)", + "name": "Run TaxReporterCLI", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/StatementParser/TaxReporterCLI/bin/Debug/netcoreapp3.0/TaxReporterCLI.dll", + "args": ["-x", "/Users/vladimiraubrecht/Documents/Taxes/2019/Report2019.xlsx", "/Users/vladimiraubrecht/Documents/Taxes/2019/Statements"], + //"args": ["/Users/vladimiraubrecht/Downloads/dividends.csv"], + "cwd": "${workspaceFolder}/StatementParser/TaxReporterCLI", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": "Run StatementParserCLI", "type": "coreclr", "request": "launch", "preLaunchTask": "build", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 17c51ad..8358939 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -7,7 +7,7 @@ "type": "process", "args": [ "build", - "${workspaceFolder}/StatementParser/StatementParserCLI/StatementParserCLI.csproj", + "${workspaceFolder}/StatementParser/StatementParser.sln", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], @@ -19,7 +19,7 @@ "type": "process", "args": [ "publish", - "${workspaceFolder}/StatementParser/StatementParserCLI/StatementParserCLI.csproj", + "${workspaceFolder}/StatementParser/StatementParser.sln", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], diff --git a/StatementParser/ExchangeRateProvider/Providers/Czk/KurzyCzProvider.cs b/StatementParser/ExchangeRateProvider/Providers/Czk/KurzyCzProvider.cs index b199e9f..31d0bf2 100644 --- a/StatementParser/ExchangeRateProvider/Providers/Czk/KurzyCzProvider.cs +++ b/StatementParser/ExchangeRateProvider/Providers/Czk/KurzyCzProvider.cs @@ -50,13 +50,6 @@ public async Task FetchCurrencyListByDateAsync(DateTime date) var name = this.SanitizeValue(cellNode[0].SelectNodes("a/span")[1].InnerHtml); var code = this.SanitizeValue(cellNode[2].InnerHtml); var amount = Convert.ToDecimal(this.SanitizeValue(cellNode[3].InnerHtml)); - - if (amount == 0) - { - // Kurzy.cz has bug on website for Indonesia in 2013. Reported to them, hopefully they fix it soon. - continue; - } - var price = Decimal.Parse(this.SanitizeValue(cellNode[4].InnerHtml), System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.GetCultureInfo("en-US")); output.Add(new CurrencyDescriptor(code, name, price, amount, country)); diff --git a/StatementParser/StatementParser/Models/Currency.cs b/StatementParser/StatementParser/Models/Currency.cs index 5c27cf8..f95ea1e 100644 --- a/StatementParser/StatementParser/Models/Currency.cs +++ b/StatementParser/StatementParser/Models/Currency.cs @@ -1,5 +1,5 @@ using System; namespace StatementParser.Models { - public enum Currency { USD, EUR, JPY } + public enum Currency { CZK, USD, EUR, JPY } } diff --git a/StatementParser/StatementParser/Models/DepositTransaction.cs b/StatementParser/StatementParser/Models/DepositTransaction.cs index b777e50..ba953f7 100644 --- a/StatementParser/StatementParser/Models/DepositTransaction.cs +++ b/StatementParser/StatementParser/Models/DepositTransaction.cs @@ -13,6 +13,11 @@ public DepositTransaction(Broker broker, DateTime date, string name, decimal amo this.Price = price; } + public override Transaction ConvertToCurrency(Currency currency, decimal exchangeRate) + { + return new DepositTransaction(Broker, Date, Name, Amount, Price * exchangeRate, currency); + } + public override string ToString() { return $"{base.ToString()} Amount: {Amount} Price: {Price}"; diff --git a/StatementParser/StatementParser/Models/DividendTransaction.cs b/StatementParser/StatementParser/Models/DividendTransaction.cs index 14f5840..8af66ea 100644 --- a/StatementParser/StatementParser/Models/DividendTransaction.cs +++ b/StatementParser/StatementParser/Models/DividendTransaction.cs @@ -16,6 +16,11 @@ public DividendTransaction(Broker broker, DateTime date, string name, decimal in this.Tax = tax; } + public override Transaction ConvertToCurrency(Currency currency, decimal exchangeRate) + { + return new DividendTransaction(Broker, Date, Name, Income * exchangeRate, Tax * exchangeRate, currency); + } + public override string ToString() { return $"{base.ToString()} Income: {Income} Tax: {Tax}"; diff --git a/StatementParser/StatementParser/Models/ESPPTransaction.cs b/StatementParser/StatementParser/Models/ESPPTransaction.cs index 4cb01cc..d48cce5 100644 --- a/StatementParser/StatementParser/Models/ESPPTransaction.cs +++ b/StatementParser/StatementParser/Models/ESPPTransaction.cs @@ -14,6 +14,11 @@ public ESPPTransaction(Broker broker, DateTime date, string name, Currency curre this.Amount = amount; } + public override Transaction ConvertToCurrency(Currency currency, decimal exchangeRate) + { + return new ESPPTransaction(Broker, Date, Name, currency, PurchasePrice * exchangeRate, MarketPrice * exchangeRate, Amount); + } + public override string ToString() { return $"{base.ToString()} Purchase Price: {PurchasePrice} Market Price: {MarketPrice} Amount: {Amount}"; diff --git a/StatementParser/StatementParser/Models/SaleTransaction.cs b/StatementParser/StatementParser/Models/SaleTransaction.cs index 37f87f3..f7e5629 100644 --- a/StatementParser/StatementParser/Models/SaleTransaction.cs +++ b/StatementParser/StatementParser/Models/SaleTransaction.cs @@ -24,6 +24,11 @@ public SaleTransaction(Broker broker, DateTime date, string name, Currency curre public decimal Swap { get; } public decimal Profit { get; } + public override Transaction ConvertToCurrency(Currency currency, decimal exchangeRate) + { + return new SaleTransaction(Broker, Date, Name, currency, Laverage, Amount, PurchasePrice * exchangeRate, SalePrice * exchangeRate, Commission * exchangeRate, Taxes * exchangeRate, Swap * exchangeRate, Profit * exchangeRate); + } + public override string ToString() { return $"{base.ToString()} {nameof(Laverage)} {Laverage} {nameof(Amount)}: {Amount} {nameof(PurchasePrice)} {PurchasePrice} {nameof(SalePrice)} {SalePrice} {nameof(Commission)} {Commission} {nameof(Taxes)} {Taxes} {nameof(Swap)} {Swap} {nameof(Profit)} {Profit}"; diff --git a/StatementParser/StatementParser/Models/Transaction.cs b/StatementParser/StatementParser/Models/Transaction.cs index 2db60d6..1b87f31 100644 --- a/StatementParser/StatementParser/Models/Transaction.cs +++ b/StatementParser/StatementParser/Models/Transaction.cs @@ -1,7 +1,7 @@ using System; namespace StatementParser.Models { - public class Transaction + public abstract class Transaction { public Broker Broker { get; } public DateTime Date { get; } @@ -16,6 +16,8 @@ public Transaction(Broker broker, DateTime date, string name, Currency currency) this.Currency = currency; } + public abstract Transaction ConvertToCurrency(Currency currency, decimal exchangeRate); + public override string ToString() { return $"Broker: {Broker} Date: {Date.ToShortDateString()} Name: {Name} Currency: {Currency}"; diff --git a/StatementParser/TaxReporterCLI/Options.cs b/StatementParser/TaxReporterCLI/Options.cs index 86d7943..99e9e70 100644 --- a/StatementParser/TaxReporterCLI/Options.cs +++ b/StatementParser/TaxReporterCLI/Options.cs @@ -4,10 +4,13 @@ namespace TaxReporterCLI { internal class Options { - [Parameter("j", "json", Description = "Switch output format into json.")] + [Parameter("j", "json", Required = Required.No, Description = "Switch output format into json.")] public bool ShouldPrintAsJson { get; set; } = false; - [PositionalParameter(0, "inputStatementFilePath", Description = "Relative or absolute path to statement file or multiple paths each as one argument.")] + [Parameter("x", "excelSheetPath", Required = Required.No, Description = "Absolute or relative path to file where excel sheet will be stored.")] + public string ExcelSheetPath { get; set; } = null; + + [PositionalParameter(0, "inputStatementFilePath", Required = Required.Yes, Description = "Relative or absolute path to statement file or multiple paths each as one argument.")] [PositionalParameterList] public string[] StatementFilePaths { get; set; } } diff --git a/StatementParser/TaxReporterCLI/Output.cs b/StatementParser/TaxReporterCLI/Output.cs new file mode 100644 index 0000000..7e8ea68 --- /dev/null +++ b/StatementParser/TaxReporterCLI/Output.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json; +using NPOI.SS.UserModel; +using NPOI.XSSF.UserModel; +using StatementParser.Models; + +namespace TaxReporterCLI +{ + public class Output + { + private Dictionary> GroupTransactions(IList transactions) + { + return transactions.GroupBy(i => i.GetType()).ToDictionary(k => k.Key.Name, i => i.Select(a => a).ToList()); + } + + public void PrintAsJson(IList transactions) + { + var groupedTransactions = GroupTransactions(transactions); + + Console.WriteLine(JsonConvert.SerializeObject(groupedTransactions)); + } + + public void SaveAsExcelSheet(string filePath, IList transactions) + { + var groupedTransactions = GroupTransactions(transactions); + + using (FileStream file = new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite)) + { + var wb1 = new XSSFWorkbook(); + + foreach (var group in groupedTransactions) + { + var sheet = wb1.CreateSheet(group.Key); + + var headerRow = sheet.CreateRow(0); + var headerProperties = GetPublicProperties(group.Value[0]); + SetRowValues(headerRow, headerProperties.Keys); + + for (int rowIndex = 1; rowIndex < group.Value.Count() + 1; rowIndex++) + { + var row = sheet.CreateRow(rowIndex); + var properties = GetPublicProperties(group.Value[rowIndex - 1]); + + SetRowValues(row, properties.Values); + } + wb1.Add(sheet); + } + + wb1.Write(file); + } + } + + public void PrintAsPlainText(IList transactions) + { + var groupedTransactions = GroupTransactions(transactions); + + foreach (var group in groupedTransactions) + { + Console.WriteLine(); + Console.WriteLine(group.Key); + Console.WriteLine(String.Join("\r\n", group.Value)); + } + } + + private void SetRowValues(IRow row, ICollection rowValues) + { + var columnIndex = 0; + foreach (var rowValue in rowValues) + { + var cell = row.CreateCell(columnIndex); + + cell.SetCellValue(rowValue); + + columnIndex++; + } + } + + private IDictionary GetPublicProperties(Transaction transaction) + { + var properties = transaction.GetType().GetProperties(); + return properties.Reverse().ToDictionary(k => k.Name, i => i.GetValue(transaction).ToString()); + } + } +} diff --git a/StatementParser/TaxReporterCLI/Program.cs b/StatementParser/TaxReporterCLI/Program.cs index 1548f54..750d26b 100644 --- a/StatementParser/TaxReporterCLI/Program.cs +++ b/StatementParser/TaxReporterCLI/Program.cs @@ -8,6 +8,7 @@ using ExchangeRateProvider.Models; using ExchangeRateProvider.Providers; using ExchangeRateProvider.Providers.Czk; +using StatementParser; using StatementParser.Models; namespace TaxReporterCLI @@ -54,83 +55,68 @@ private static async Task RunAsync(Options option) var cnbProvider = new CzechNationalBankProvider(); var kurzyCzProvider = new KurzyCzProvider(); - var parser = new StatementParser.TransactionParser(); + var parser = new TransactionParser(); + var transactions = new List(); var filePaths = ResolveFilePaths(option.StatementFilePaths); - foreach (var file in filePaths) { + Console.WriteLine($"Processing file: {file}"); var result = await parser.ParseAsync(file); - if (result == null) + if (result != null) { - continue; + transactions.AddRange(result); } + } - var kurzyPerYear = await FetchExchangeRatesForEveryYearAsync(kurzyCzProvider, result); - - foreach (var transaction in result) - { - var cnbCurrencyList = await cnbProvider.FetchCurrencyListByDateAsync(transaction.Date); - var cnbPrice = cnbCurrencyList[transaction.Currency.ToString()].Price; + var kurzyPerYear = await FetchExchangeRatesForEveryYearAsync(kurzyCzProvider, transactions); - // TODO: Refactor this, it's ugly like hell. - if (transaction is DepositTransaction) + var transactionsByPerYearExchangeRate = + transactions.Select(i => { + if (!kurzyPerYear[i.Date.Year].IsEmpty) { - var castedTransaction = transaction as DepositTransaction; - - if (!kurzyPerYear[transaction.Date.Year].IsEmpty) - { - var kurzyPrice = kurzyPerYear[transaction.Date.Year][transaction.Currency.ToString()].Price; - Console.WriteLine($"{transaction} Price in CZK (CNB): {castedTransaction.Price * cnbPrice} Price in CZK (year average): {castedTransaction.Price * kurzyPrice}"); - } - else - { - Console.WriteLine($"{transaction} Price in CZK (CNB): {castedTransaction.Price * cnbPrice} Price in CZK (year average): N/A"); - } + var exchangeRatio = kurzyPerYear[i.Date.Year][i.Currency.ToString()].Price; + return i.ConvertToCurrency(Currency.CZK, exchangeRatio); } - else if (transaction is DividendTransaction) - { - var castedTransaction = transaction as DividendTransaction; - - if (!kurzyPerYear[transaction.Date.Year].IsEmpty) - { - var kurzyPrice = kurzyPerYear[transaction.Date.Year][transaction.Currency.ToString()].Price; - Console.WriteLine($"{transaction} Income in CZK (CNB): {castedTransaction.Income * cnbPrice} Income in CZK (year average): {castedTransaction.Income * kurzyPrice} Tax in CZK (CNB): {castedTransaction.Tax * cnbPrice} Tax in CZK (year average): {castedTransaction.Tax * kurzyPrice}"); - } - else - { - Console.WriteLine($"{transaction} Income in CZK (CNB): {castedTransaction.Income * cnbPrice} Income in CZK (year average): N/A Tax in CZK (CNB): {castedTransaction.Tax * cnbPrice} Tax in CZK (year average): N/A"); - } - } - else if (transaction is ESPPTransaction) - { - var castedTransaction = transaction as ESPPTransaction; - - if (!kurzyPerYear[transaction.Date.Year].IsEmpty) - { - var kurzyPrice = kurzyPerYear[transaction.Date.Year][transaction.Currency.ToString()].Price; - Console.WriteLine($"{transaction} Purchase Price in CZK (CNB): {castedTransaction.PurchasePrice * cnbPrice} Purchase Price in CZK (year average): {castedTransaction.PurchasePrice * kurzyPrice} Market Price in CZK (CNB): {castedTransaction.MarketPrice * cnbPrice} Market Price in CZK (year average): {castedTransaction.MarketPrice * kurzyPrice}"); - } - else - { - Console.WriteLine($"{transaction} Purchase Price in CZK (CNB): {castedTransaction.PurchasePrice * cnbPrice} Purchase Price in CZK (year average): N/A Market Price in CZK (CNB): {castedTransaction.MarketPrice * cnbPrice} Market Price in CZK (year average): N/A"); - } - } - else if (transaction is SaleTransaction) - { - var castedTransaction = transaction as SaleTransaction; - - if (!kurzyPerYear[transaction.Date.Year].IsEmpty) - { - var kurzyPrice = kurzyPerYear[transaction.Date.Year][transaction.Currency.ToString()].Price; - Console.WriteLine($"{transaction} Purchase Price in CZK (CNB): {castedTransaction.PurchasePrice * cnbPrice} Purchase Price in CZK (year average): {castedTransaction.PurchasePrice * kurzyPrice} Sale Price in CZK (CNB): {castedTransaction.SalePrice * cnbPrice} Sale Price in CZK (year average): {castedTransaction.SalePrice * kurzyPrice}"); - } - else - { - Console.WriteLine($"{transaction} Purchase Price in CZK (CNB): {castedTransaction.PurchasePrice * cnbPrice} Purchase Price in CZK (year average): N/A Sale Price in CZK (CNB): {castedTransaction.SalePrice * cnbPrice} Sale Price in CZK (year average): N/A"); - } - } - } + + return i; + + }).ToList(); + + var transactionsByPerDayExchangeRateTasks = + transactions.Select(async i => { + var cnbCurrencyList = await cnbProvider.FetchCurrencyListByDateAsync(i.Date); + return i.ConvertToCurrency(Currency.CZK, cnbCurrencyList[i.Currency.ToString()].Price); + }).ToList(); + + await Task.WhenAll(transactionsByPerDayExchangeRateTasks); + var transactionsByPerDayExchangeRate = transactionsByPerDayExchangeRateTasks.Select(i => i.Result).ToList(); + + var pathBackup = option.ExcelSheetPath; + + Console.WriteLine("Transactions calculated with yearly average exchange rate:"); + option.ExcelSheetPath = Path.ChangeExtension(pathBackup, "yearly.xlsx"); + Print(option, transactionsByPerYearExchangeRate); + + Console.WriteLine("\r\nTransactions calculated with daily exchange rate:"); + option.ExcelSheetPath = Path.ChangeExtension(pathBackup, "daily.xlsx"); + Print(option, transactionsByPerDayExchangeRate); + } + + private static void Print(Options option, IList transactions) + { + var printer = new Output(); + if (option.ShouldPrintAsJson) + { + printer.PrintAsJson(transactions); + } + else if (option.ExcelSheetPath != null) + { + printer.SaveAsExcelSheet(option.ExcelSheetPath, transactions); + } + else + { + printer.PrintAsPlainText(transactions); } }