Skip to content

Commit

Permalink
TaxReporterCLI simplified and it can print xlsx now.
Browse files Browse the repository at this point in the history
  • Loading branch information
Vladimír Aubrecht authored and Vladimír Aubrecht committed Feb 4, 2020
1 parent 35de7df commit e4d43b7
Show file tree
Hide file tree
Showing 12 changed files with 185 additions and 83 deletions.
19 changes: 15 additions & 4 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"type": "process",
"args": [
"build",
"${workspaceFolder}/StatementParser/StatementParserCLI/StatementParserCLI.csproj",
"${workspaceFolder}/StatementParser/StatementParser.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
Expand All @@ -19,7 +19,7 @@
"type": "process",
"args": [
"publish",
"${workspaceFolder}/StatementParser/StatementParserCLI/StatementParserCLI.csproj",
"${workspaceFolder}/StatementParser/StatementParser.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,6 @@ public async Task<CurrencyList> 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));
Expand Down
2 changes: 1 addition & 1 deletion StatementParser/StatementParser/Models/Currency.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using System;
namespace StatementParser.Models
{
public enum Currency { USD, EUR, JPY }
public enum Currency { CZK, USD, EUR, JPY }
}
5 changes: 5 additions & 0 deletions StatementParser/StatementParser/Models/DepositTransaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}";
Expand Down
5 changes: 5 additions & 0 deletions StatementParser/StatementParser/Models/DividendTransaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}";
Expand Down
5 changes: 5 additions & 0 deletions StatementParser/StatementParser/Models/ESPPTransaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}";
Expand Down
5 changes: 5 additions & 0 deletions StatementParser/StatementParser/Models/SaleTransaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}";
Expand Down
4 changes: 3 additions & 1 deletion StatementParser/StatementParser/Models/Transaction.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
namespace StatementParser.Models
{
public class Transaction
public abstract class Transaction
{
public Broker Broker { get; }
public DateTime Date { get; }
Expand All @@ -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}";
Expand Down
7 changes: 5 additions & 2 deletions StatementParser/TaxReporterCLI/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
Expand Down
87 changes: 87 additions & 0 deletions StatementParser/TaxReporterCLI/Output.cs
Original file line number Diff line number Diff line change
@@ -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<string, List<Transaction>> GroupTransactions(IList<Transaction> transactions)
{
return transactions.GroupBy(i => i.GetType()).ToDictionary(k => k.Key.Name, i => i.Select(a => a).ToList());
}

public void PrintAsJson(IList<Transaction> transactions)
{
var groupedTransactions = GroupTransactions(transactions);

Console.WriteLine(JsonConvert.SerializeObject(groupedTransactions));
}

public void SaveAsExcelSheet(string filePath, IList<Transaction> 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<Transaction> 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<string> rowValues)
{
var columnIndex = 0;
foreach (var rowValue in rowValues)
{
var cell = row.CreateCell(columnIndex);

cell.SetCellValue(rowValue);

columnIndex++;
}
}

private IDictionary<string, string> GetPublicProperties(Transaction transaction)
{
var properties = transaction.GetType().GetProperties();
return properties.Reverse().ToDictionary(k => k.Name, i => i.GetValue(transaction).ToString());
}
}
}
118 changes: 52 additions & 66 deletions StatementParser/TaxReporterCLI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using ExchangeRateProvider.Models;
using ExchangeRateProvider.Providers;
using ExchangeRateProvider.Providers.Czk;
using StatementParser;
using StatementParser.Models;

namespace TaxReporterCLI
Expand Down Expand Up @@ -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<Transaction>();
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<Transaction> 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);
}
}

Expand Down

0 comments on commit e4d43b7

Please sign in to comment.