Summering av aktiebidrag så att endast en post för varje aktie visas. Sortering genom click på rubrik i listviews

This commit is contained in:
2026-05-31 08:46:27 +02:00
parent 64fe7aafaf
commit 1202a513d9
10 changed files with 299 additions and 36 deletions

View File

@ -0,0 +1,62 @@
namespace StockHistory;
public static class ListViewExtensions
{
public static void EnableSmartSorting(this ListView lv)
{
lv.ColumnClick += (s, e) =>
{
if (lv.Tag is not SortState state)
{
state = new SortState();
lv.Tag = state;
}
if (state.Column == e.Column)
{
state.Order = state.Order == SortOrder.Ascending
? SortOrder.Descending
: SortOrder.Ascending;
}
else
{
state.Column = e.Column;
state.Order = SortOrder.Ascending;
}
lv.ListViewItemSorter = new SmartListViewComparer(state.Column, state.Order);
lv.Sort();
UpdateHeaderArrows(lv, state.Column, state.Order);
HighlightSortedColumn(lv, state.Column);
};
}
private static void UpdateHeaderArrows(ListView lv, int col, SortOrder order)
{
foreach (ColumnHeader ch in lv.Columns)
ch.Text = ch.Text.Replace(" ▲", "").Replace(" ▼", "");
string arrow = order == SortOrder.Ascending ? " ▲" : " ▼";
lv.Columns[col].Text += arrow;
}
private static void HighlightSortedColumn(ListView lv, int col)
{
foreach (ListViewItem item in lv.Items)
{
for (int i = 0; i < item.SubItems.Count; i++)
{
item.SubItems[i].BackColor = i == col
? Color.FromArgb(235, 245, 255)
: Color.White;
}
}
}
private class SortState
{
public int Column = -1;
public SortOrder Order = SortOrder.None;
}
}

View File

@ -0,0 +1,31 @@
using System.Collections;
namespace StockHistory;
public class ListViewItemComparer : IComparer
{
private int col;
private SortOrder order;
public ListViewItemComparer(int column, SortOrder order)
{
col = column;
this.order = order;
}
public int Compare(object x, object y)
{
string a = ((ListViewItem)x).SubItems[col].Text;
string b = ((ListViewItem)y).SubItems[col].Text;
int result;
// Försök sortera som tal först
if (double.TryParse(a, out double da) && double.TryParse(b, out double db))
result = da.CompareTo(db);
else
result = String.Compare(a, b);
return order == SortOrder.Ascending ? result : -result;
}
}

View File

@ -0,0 +1,10 @@
namespace StockHistory.Models;
public class EquityTemp
{
public string StockCode { get; set; } = string.Empty;
public DateTime Bought { get; set; }
public decimal BoughtPrice { get; set; }
public int Quantity { get; set; }
public decimal TotalValue => BoughtPrice * Quantity;
}

View File

@ -38,7 +38,7 @@ namespace StockHistory
ServiceLifetime.Transient);
services.AddSingleton<frmStockHistoryInit>();
services.AddScoped<frmStockHistoryAnalyse>();
services.AddTransient<frmStockHistoryAnalyse>();
services.AddTransient<IPdfOpener, PdfOpener>();
services.AddTransient<IPdfFormatter, PdfFormatter>();
services.AddTransient<ITransactionNotesServices, TransactionNotesServices>();

View File

@ -22,17 +22,20 @@ public class StockEquityServices : IStockEquityServices
public async Task<List<StockEquity>> GetAllAsync(DateTime equityDate)
{
return await _dbContext.Stocks
.Where(s => s.Bought <= equityDate && s.Sold > equityDate)
.Where(s => s.Bought <= equityDate && (s.Sold > equityDate || s.SoldQuant == 0))
.ToListAsync();
}
public async Task GenerateStockVision()
{
var existingNotes = await _notesServices.GetAllAsync();
// foreach (var note in await _notesServices.GetAllAsync())
foreach (var note in existingNotes)
await _dbContext.Stocks.ExecuteDeleteAsync();
await _dbContext.SaveChangesAsync();
var existingNotes = await _notesServices.GetAllAsync();
foreach (var note in existingNotes.OrderBy(e => e.TransactionDate).ToList())
{
if (note.TransactionType == null) { }
if (note.TransactionType == null && note.StockCode != null) { }
else
if (note.TransactionType.ToLower() == "köp")
{
@ -51,31 +54,49 @@ public class StockEquityServices : IStockEquityServices
}
else if (note.TransactionType.ToLower() == "sälj" || note.TransactionType.ToLower() == "försäljning")
{
var equity = await _dbContext.Stocks.FirstOrDefaultAsync(s => s.StockCode == note.StockCode && s.SoldQuant == 0);
var equityList = await _dbContext.Stocks.Where(s => s.StockCode == note.StockCode && s.SoldQuant == 0).OrderBy(s => s.Quantity).ToListAsync();
if (equity != null)
if (equityList != null)
{
if (note.StockQuantity < equity.Quantity)
var soldQuant = Math.Abs(note.StockQuantity);
foreach (var eq in equityList.OrderBy(e => e.Quantity))
{
if (soldQuant == 0)
{
break;
}
else
{
if (eq.Quantity <= soldQuant)
{
eq.Sold = note.TransactionDate;
eq.SoldPrice = note.StockPrice;
eq.SoldQuant = eq.Quantity;
soldQuant -= eq.Quantity;
_dbContext.Stocks.Update(eq);
await _dbContext.SaveChangesAsync();
}
else
{
var splitEquity = new StockEquity
{
StockCode = equity.StockCode,
Bought = equity.Bought,
BoughtPrice = equity.BoughtPrice,
Quantity = equity.Quantity - Math.Abs(note.StockQuantity),
Sold = DateTime.MinValue,
SoldPrice = 0m,
SoldQuant = 0
StockCode = eq.StockCode,
Bought = eq.Bought,
BoughtPrice = eq.BoughtPrice,
Quantity = soldQuant,
Sold = note.TransactionDate,
SoldPrice = note.StockPrice,
SoldQuant = soldQuant
};
_dbContext.Stocks.Add(splitEquity);
await _dbContext.SaveChangesAsync();
}
equity.Quantity = Math.Abs(note.StockQuantity);
equity.Sold = note.TransactionDate;
equity.SoldPrice = note.StockPrice;
equity.SoldQuant = Math.Abs(note.StockQuantity);
_dbContext.Stocks.Update(equity);
eq.Quantity -= soldQuant;
_dbContext.Stocks.Update(eq);
await _dbContext.SaveChangesAsync();
soldQuant = 0;
}
}
}
}
}

View File

@ -0,0 +1,63 @@
using System.Collections;
using System.Runtime.InteropServices;
namespace StockHistory;
public class SmartListViewComparer : IComparer
{
private readonly int col;
private readonly SortOrder order;
public SmartListViewComparer(int column, SortOrder order)
{
col = column;
this.order = order;
}
public int Compare(object x, object y)
{
string a = ((ListViewItem)x).SubItems[col].Text;
string b = ((ListViewItem)y).SubItems[col].Text;
// Tomma värden sist
if (string.IsNullOrWhiteSpace(a) && string.IsNullOrWhiteSpace(b)) return 0;
if (string.IsNullOrWhiteSpace(a)) return order == SortOrder.Ascending ? 1 : -1;
if (string.IsNullOrWhiteSpace(b)) return order == SortOrder.Ascending ? -1 : 1;
int result = CompareSmart(a, b);
return order == SortOrder.Ascending ? result : -result;
}
private int CompareSmart(string a, string b)
{
// Procent
if (a.EndsWith("%") && b.EndsWith("%") &&
double.TryParse(a.TrimEnd('%'), out double pa) &&
double.TryParse(b.TrimEnd('%'), out double pb))
return pa.CompareTo(pb);
// Valuta
string ac = a.Replace("kr", "").Replace("$", "").Replace("€", "").Trim();
string bc = b.Replace("kr", "").Replace("$", "").Replace("€", "").Trim();
if (double.TryParse(ac, out double ca) && double.TryParse(bc, out double cb))
return ca.CompareTo(cb);
// Datum
if (DateTime.TryParse(a, out DateTime da) && DateTime.TryParse(b, out DateTime db))
return da.CompareTo(db);
// Tal
if (double.TryParse(a, out double na) && double.TryParse(b, out double nb))
return na.CompareTo(nb);
// Naturlig sortering
return StrCmpLogicalW(a, b);
}
[System.Runtime.InteropServices.DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
private static extern int StrCmpLogicalW(string x, string y);
}

View File

@ -37,6 +37,8 @@
chBought = new ColumnHeader();
chPrize = new ColumnHeader();
chNumber = new ColumnHeader();
lblTotalValue = new Label();
chValue = new ColumnHeader();
SuspendLayout();
//
// lblHistAnalyseHeader
@ -87,7 +89,7 @@
//
// lwStocks
//
lwStocks.Columns.AddRange(new ColumnHeader[] { chStock, chBought, chPrize, chNumber });
lwStocks.Columns.AddRange(new ColumnHeader[] { chStock, chBought, chPrize, chNumber, chValue });
lwStocks.Location = new Point(31, 110);
lwStocks.Name = "lwStocks";
lwStocks.Size = new Size(676, 172);
@ -115,11 +117,25 @@
chNumber.Text = "Antal";
chNumber.Width = 100;
//
// lblTotalValue
//
lblTotalValue.AutoSize = true;
lblTotalValue.Location = new Point(345, 295);
lblTotalValue.Name = "lblTotalValue";
lblTotalValue.Size = new Size(16, 15);
lblTotalValue.TabIndex = 10;
lblTotalValue.Text = "...";
//
// chValue
//
chValue.Text = "Total";
//
// frmStockHistoryAnalyse
//
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(800, 450);
Controls.Add(lblTotalValue);
Controls.Add(lwStocks);
Controls.Add(btnClose);
Controls.Add(dtpChosenDate);
@ -142,5 +158,7 @@
private ColumnHeader chBought;
private ColumnHeader chPrize;
private ColumnHeader chNumber;
private Label lblTotalValue;
private ColumnHeader chValue;
}
}

View File

@ -1,4 +1,5 @@
using StockHistory.Services;
using StockHistory.Models;
using StockHistory.Services;
namespace StockHistory;
@ -10,16 +11,20 @@ public partial class frmStockHistoryAnalyse : Form
{
InitializeComponent();
_equityServices = equityServices;
lwStocks.EnableSmartSorting();
}
private void btnGenerateStockScheme_Click(object sender, EventArgs e)
{
_equityServices.GenerateStockVision().Wait();
dtpChosenDate.Value = DateTime.Today;
LoadStockListAsync(dtpChosenDate.Value).Wait();
}
private void btnClose_Click(object sender, EventArgs e)
{
this.Close();
this.Hide();
}
private void dtpChosenDate_ValueChanged(object sender, EventArgs e)
@ -34,14 +39,60 @@ public partial class frmStockHistoryAnalyse : Form
// var stocks = await _equityServices.GetAllAsync();
lwStocks.Items.Clear();
foreach (var t in stocks)
var dTotal = stocks.Sum(s => s.BoughtPrice * s.Quantity);
lblTotalValue.Text = dTotal.ToString("C");
EquityTemp equityTemp = new EquityTemp
{
var item = new ListViewItem(t.StockCode);
item.SubItems.Add(t.Bought.ToString());
item.SubItems.Add(t.BoughtPrice.ToString());
item.SubItems.Add(t.Quantity.ToString());
StockCode = "Total",
Bought = DateTime.MinValue,
BoughtPrice = 0,
Quantity = 0
};
foreach (var t in stocks.OrderBy(s => s.StockCode + s.Bought).ToList())
{
if (t.StockCode != equityTemp.StockCode)
{
if (equityTemp.StockCode != "Total")
{
var item = new ListViewItem(equityTemp.StockCode);
item.SubItems.Add(equityTemp.Bought.ToString());
item.SubItems.Add(equityTemp.BoughtPrice.ToString());
item.SubItems.Add(equityTemp.Quantity.ToString());
item.SubItems.Add((equityTemp.TotalValue).ToString("C"));
lwStocks.Items.Add(item);
}
equityTemp.StockCode = t.StockCode;
equityTemp.Bought = t.Bought;
equityTemp.BoughtPrice = t.BoughtPrice;
equityTemp.Quantity = t.Quantity;
continue;
}
else
{
if (t.Bought < equityTemp.Bought)
equityTemp.Bought = t.Bought;
if (t.BoughtPrice < equityTemp.BoughtPrice || equityTemp.BoughtPrice == 0)
equityTemp.BoughtPrice = t.BoughtPrice;
equityTemp.Quantity += t.Quantity;
}
}
var l_item = new ListViewItem(equityTemp.StockCode);
l_item.SubItems.Add(equityTemp.Bought.ToString());
l_item.SubItems.Add(equityTemp.BoughtPrice.ToString());
l_item.SubItems.Add(equityTemp.Quantity.ToString());
l_item.SubItems.Add((equityTemp.TotalValue).ToString("C"));
lwStocks.Items.Add(l_item);
}
protected override void OnVisibleChanged(EventArgs e)
{
base.OnVisibleChanged(e);
if (this.Visible)
{
dtpChosenDate.Value = DateTime.Today;
LoadStockListAsync(dtpChosenDate.Value).Wait();
}
}
}

View File

@ -122,6 +122,7 @@
lwTransactions.TabIndex = 6;
lwTransactions.UseCompatibleStateImageBehavior = false;
lwTransactions.View = View.Details;
// lwTransactions.ColumnClick += lwTransactions_ColumnClick;
//
// StockCodeHeader
//
@ -224,7 +225,7 @@
Controls.Add(btnChooseFile);
Margin = new Padding(3, 2, 3, 2);
Name = "frmStockHistoryInit";
Text = "StockBrokerage";
Text = "29";
ResumeLayout(false);
PerformLayout();
}

View File

@ -20,6 +20,9 @@ public partial class frmStockHistoryInit : Form
private TransactionNote trans = new();
private bool SHB = false;
private string pdfFilePath = string.Empty;
// Sortering
private int _sortColumn = -1;
private SortOrder _sortOrder = SortOrder.None;
public frmStockHistoryInit(
IPdfOpener pdfOpener,
@ -30,6 +33,7 @@ public partial class frmStockHistoryInit : Form
)
{
InitializeComponent();
lwTransactions.EnableSmartSorting();
_pdfOpener = pdfOpener;
_pdfFormatter = pdfFormatter;
_transactionNotes = transactionNotes;
@ -299,5 +303,7 @@ public partial class frmStockHistoryInit : Form
{
_analyseForm.Show();
}
}