-
Notifications
You must be signed in to change notification settings - Fork 4
/
Investments.cs
511 lines (484 loc) · 19.5 KB
/
Investments.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using CodeFirstWebFramework;
namespace AccountServer {
public class Investments : BankingAccounting {
protected override void Init() {
base.Init();
InsertMenuOptions(
new MenuOption("Listing", "/investments/default.html"),
new MenuOption("Securities", "/investments/securities.html")
);
if (!SecurityOn || UserAccessLevel >= AccessLevel.ReadWrite)
InsertMenuOptions(
new MenuOption("New Account", "/investments/detail.html?id=0")
);
}
/// <summary>
/// List all security accounts, with their cash balance and current security value
/// </summary>
public object DefaultListing() {
return Database.Query(@"SELECT Account.*, Amount AS CashBalance, Value
FROM Account
LEFT JOIN (SELECT AccountId, SUM(Amount) AS Amount FROM Journal GROUP BY AccountId) AS Balances ON Balances.AccountId = idAccount
JOIN AccountType ON idAccountType = AccountTypeId
LEFT JOIN (" + AccountValue(Database, Utils.Today) + @") AS AccountValues ON AccountValues.ParentAccountId = Balances.AccountId
WHERE AccountTypeId = " + (int)AcctType.Investment + @"
GROUP BY idAccount ORDER BY AccountName");
}
/// <summary>
/// Retrieve a security account for editing
/// </summary>
public void Detail(int id) {
InvestmentDetail record = Database.QueryOne<InvestmentDetail>("Account.*, AcctType, SUM(Amount) AS CashBalance",
"WHERE idAccount = " + id,
"Account", "Journal");
record.CurrentBalance = record.CashBalance - Database.QueryOne("SELECT SUM(Amount) AS Future FROM Journal JOIN Document ON idDocument = DocumentId WHERE AccountId = "
+ id + " AND DocumentDate > " + Database.Quote(Utils.Today)).AsDecimal("Future");
record.Value = Database.QueryOne("SELECT Value FROM (" + AccountValue(Database, Utils.Today) + ") AS V WHERE ParentAccountid = " + id).AsDecimal("Value");
if (record.Id == null) {
record.AccountTypeId = (int)AcctType.Investment;
} else {
checkAcctType(record.AccountTypeId, AcctType.Investment);
Title += " - " + record.AccountName;
}
Record = record;
}
/// <summary>
/// List all transactions for account
/// </summary>
public IEnumerable<JObject> DetailListing(int id) {
return detailsWithBalance(id).Reverse();
}
/// <summary>
/// Update account info after editing
/// </summary>
public AjaxReturn DetailSave(Account json) {
checkAcctType(json.AccountTypeId, AcctType.Investment);
return SaveRecord(json, true);
}
/// <summary>
/// Portfolio header - same as Detail header
/// </summary>
public void Portfolio(int id) {
Detail(id);
}
/// <summary>
/// List all securities for account, with quantity and current value
/// </summary>
public IEnumerable<JObject> PortfolioListing(int id) {
return (from p in Database.Query("SELECT SecurityName, AccountName, SV.* FROM ("
+ SecurityValues(Database, Utils.Today) + @") AS SV
JOIN Account ON idAccount = SV.AccountId
JOIN Security ON idSecurity = SecurityId
WHERE ParentAccountid = " + id).ToList()
let cb = SecurityCost(p.AsInt("AccountId"))
select new JObject(p).AddRange(
"CostBasis", cb,
"Change", cb == 0 ? 0 : 100 * (p.AsDecimal("Value") - cb) / cb
));
}
/// <summary>
/// Get a Buy or Sell document for editing
/// </summary>
public void Document(int id, DocType type) {
Title = Title.Replace("Document", type.UnCamel());
InvestmentDocument header = getDocument<InvestmentDocument>(id);
if (header.idDocument == null) {
header.DocumentTypeId = (int)type;
header.DocType = type.UnCamel();
header.DocumentDate = Utils.Today;
header.DocumentName = "";
if (GetParameters["acct"].IsInteger()) {
FullAccount acct = Database.QueryOne<FullAccount>("*", "WHERE idAccount = " + GetParameters["acct"], "Account");
if (acct.idAccount != null) {
header.DocumentAccountId = (int)acct.idAccount;
header.DocumentAccountName = acct.AccountName;
header.FeeAccount = Database.QueryOne("SELECT idAccount FROM Account WHERE AccountName = " + Database.Quote(acct.AccountName + " fees")).AsInt("idAccount");
}
}
} else {
checkDocType(header.DocumentTypeId, DocType.Buy, DocType.Sell);
List<JObject> journals = Database.Query(@"SELECT *
FROM Journal
LEFT JOIN StockTransaction ON idStockTransaction = idJournal
LEFT JOIN Security ON idSecurity = SecurityId
WHERE JournalNum > 1
AND DocumentId = " + id).ToList();
header.SecurityId = journals[0].AsInt("SecurityId");
header.SecurityName = journals[0].AsString("SecurityName");
header.Quantity = journals[0].AsDouble("Quantity");
header.Price = journals[0].AsDouble("Price");
if (journals.Count > 1) {
header.FeeAccount = journals[1].AsInt("AccountId");
header.Fee = journals[1].AsDecimal("Amount");
header.FeeMemo = journals[1].AsString("Memo");
}
if (type == DocType.Sell)
header.Quantity = -header.Quantity;
}
JObject record = new JObject().AddRange("header", header);
nextPreviousDocument(record, "JOIN Journal ON DocumentId = idDocument WHERE DocumentTypeId "
+ Database.In(DocType.Buy, DocType.Sell)
+ (header.DocumentAccountId > 0 ? " AND AccountId = " + header.DocumentAccountId : ""));
record.AddRange("Accounts", SelectExpenseAccounts(),
"Names", SelectOthers(),
"Securities", SelectSecurities());
Record = record;
}
public AjaxReturn DocumentDelete(int id) {
return deleteDocument(id, DocType.Buy, DocType.Sell, DocType.Transfer);
}
/// <summary>
/// Update Buy/Sell after editing
/// </summary>
public AjaxReturn DocumentSave(InvestmentDocument json) {
Database.BeginTransaction();
JObject oldDoc = getCompleteDocument(json.idDocument);
DocType t = checkDocType(json.DocumentTypeId, DocType.Buy, DocType.Sell);
FullAccount acct = Database.Get<FullAccount>((int)json.DocumentAccountId);
checkAcctType(acct.AccountTypeId, AcctType.Investment);
int sign = SignFor(t);
fixNameAddress(json, "O");
if(json.SecurityId == 0) {
Utils.Check(!string.IsNullOrEmpty(json.SecurityName), "No Security Name supplied");
json.SecurityId = Database.ForeignKey("Security", "SecurityName", json.SecurityName);
}
if (string.IsNullOrEmpty(json.DocumentMemo))
json.DocumentMemo = json.SecurityName;
if (json.idDocument == null) {
StockPrice p = Database.QueryOne<StockPrice>("SELECT * FROM " + LatestPrice(Database, json.DocumentDate) + " WHERE SecurityId = " + json.SecurityId);
if (p.Price != json.Price) {
// Stock price is different from file price, and its a new buy/sell - update file price for security date
p.SecurityId = (int)json.SecurityId;
p.Date = json.DocumentDate;
p.Price = json.Price;
Database.Update(p);
}
}
decimal cost = (decimal)Math.Round(json.Price * json.Quantity, 2);
decimal amount = json.Fee + sign * cost;
Database.Update(json);
// First journal is posting to this account
Journal journal = Database.Get(new Journal() {
DocumentId = (int)json.idDocument,
JournalNum = 1
});
journal.DocumentId = (int)json.idDocument;
journal.AccountId = json.DocumentAccountId;
journal.NameAddressId = json.DocumentNameAddressId;
journal.Memo = json.DocumentMemo;
journal.JournalNum = 1;
journal.Amount = -amount;
journal.Outstanding = -amount;
Database.Update(journal);
// Second journal is to subaccount for this security (account:security)
journal = Database.Get(new Journal() {
DocumentId = (int)json.idDocument,
JournalNum = 2
});
journal.DocumentId = (int)json.idDocument;
journal.AccountId = (int)Database.ForeignKey("Account",
"AccountName", acct.AccountName + ":" + json.SecurityName,
"AccountTypeId", (int)AcctType.Security);
journal.NameAddressId = json.DocumentNameAddressId;
journal.Memo = json.DocumentMemo;
journal.JournalNum = 2;
journal.Amount = journal.Outstanding = sign * cost;
Database.Update(journal);
// Corresponding line
Line line = Database.Get<Line>((int)journal.idJournal);
line.idLine = journal.idJournal;
line.LineAmount = cost;
Database.Update(line);
// Now update the stock transaction
StockTransaction st = Database.Get<StockTransaction>((int)journal.idJournal);
st.idStockTransaction = journal.idJournal;
st.ParentAccountId = json.DocumentAccountId;
st.SecurityId = (int)json.SecurityId;
st.Price = json.Price;
st.Quantity = sign * json.Quantity;
st.CostPer = Math.Round((double)amount / json.Quantity, 4);
Database.Update(st);
if(json.Fee != 0) {
// Need another journal and line for the fee
Utils.Check(json.FeeAccount > 0, "No Fee Account supplied");
journal = Database.Get(new Journal() {
DocumentId = (int)json.idDocument,
JournalNum = 3
});
journal.DocumentId = (int)json.idDocument;
journal.AccountId = (int)json.FeeAccount;
journal.NameAddressId = json.DocumentNameAddressId;
journal.Memo = json.FeeMemo;
journal.JournalNum = 3;
journal.Amount = journal.Outstanding = json.Fee;
Database.Update(journal);
line = Database.Get<Line>((int)journal.idJournal);
line.idLine = journal.idJournal;
line.LineAmount = sign * json.Fee;
Database.Update(line);
}
// Delete any left over lines from the old transaction
Database.Execute("DELETE FROM Line WHERE idLine IN (SELECT idJournal FROM Journal WHERE DocumentId = " + json.idDocument + " AND JournalNum > " + journal.JournalNum + ")");
Database.Execute("DELETE FROM Journal WHERE Documentid = " + json.idDocument + " AND JournalNum > " + journal.JournalNum);
// Audit
JObject newDoc = getCompleteDocument(json.idDocument);
Database.AuditUpdate("Document", json.idDocument, oldDoc, newDoc);
Database.Commit();
return new AjaxReturn() { message = "Document saved", id = json.idDocument };
}
/// <summary>
/// A balance adjustment posts enough to to reach the new balance.
/// Important fields are ExistingBalance and NewBalance
/// </summary>
public void BalanceAdjustment(int id, int acct) {
checkAccountIsAcctType(acct, AcctType.Investment);
BalanceAdjustmentDocument doc = Database.Get<BalanceAdjustmentDocument>(id);
doc.NewBalance = doc.ExistingBalance = Database.QueryOne(@"SELECT SUM(Amount) AS Amount FROM Journal WHERE AccountId = " + acct).AsDecimal("Amount");
if (doc.idDocument == null) {
doc.DocumentAccountId = acct;
doc.DocumentDate = Utils.Today;
doc.DocumentMemo = "Balance Adjustment";
if(string.IsNullOrEmpty(doc.DocumentIdentifier))
doc.DocumentIdentifier = "Balance Adjustment";
JObject o = Database.QueryOne(@"SELECT J.AccountId, AccountName
FROM Journal
JOIN Document ON idDocument = Journal.DocumentId
JOIN Journal AS J ON J.DocumentId = idDocument AND J.JournalNum = 2
JOIN Account ON idAccount = J.AccountId
WHERE Journal.JournalNum = 1
AND DocumentTypeID IN (" + (int)DocType.Withdrawal + "," + (int)DocType.Deposit + @")
AND Journal.AccountId = " + acct);
doc.AccountId = o.AsInt("AccountId");
doc.AccountName = o.AsString("AccountName");
doc.NameAddressId = 1;
doc.Name = "";
} else {
checkDocType(doc.DocumentTypeId, DocType.Withdrawal, DocType.Deposit);
foreach (Journal j in Database.Query<Journal>("SELECT * FROM Journal WHERE DocumentId = " + id)) {
switch (j.JournalNum) {
case 1:
doc.DocumentAccountId = j.AccountId;
doc.NameAddressId = j.NameAddressId;
doc.Amount = j.Amount;
break;
case 2:
doc.AccountId = j.AccountId;
break;
default:
throw new CheckException("Document is not a balance adjustment");
}
}
Utils.Check(acct == doc.DocumentAccountId, "Document is for a different account");
doc.Name = Database.QueryOne("SELECT Name FROM NameAddress WHERE idNameAddress = " + doc.NameAddressId).AsString("Name");
doc.AccountName = Database.QueryOne("SELECT AccountName FROM Account WHERE idAccount = " + doc.AccountId).AsString("AccountName");
doc.ExistingBalance -= doc.Amount;
}
Record = new JObject().AddRange(
"header", doc,
"Accounts", SelectAccounts(),
"Names", SelectOthers());
}
/// <summary>
/// Save a BalanceAdjustment after editing.
/// Transaction amount is NewBalance - ExistingBalance
/// </summary>
public AjaxReturn BalanceAdjustmentSave(BalanceAdjustmentDocument json) {
checkAccountIsAcctType(json.DocumentAccountId, AcctType.Investment);
Utils.Check(json.AccountId > 0, "No account selected");
// Pointless to post a new transaction that does nothing
Utils.Check(json.idDocument > 0 || json.NewBalance != json.ExistingBalance, "Balance is unchanged");
if (json.NameAddressId == 0)
json.NameAddressId = Database.ForeignKey("NameAddress",
"Type", "O",
"Name", json.Name);
else
checkNameType(json.NameAddressId, "O");
JObject old = getCompleteDocument(json.idDocument);
json.Amount = json.NewBalance - json.ExistingBalance;
json.DocumentTypeId = (int)(json.Amount < 0 ? DocType.Withdrawal : DocType.Deposit);
Database.BeginTransaction();
Database.Update(json);
Journal j = new Journal();
j.AccountId = json.DocumentAccountId;
j.Outstanding = j.Amount = json.Amount;
j.DocumentId = (int)json.idDocument;
j.JournalNum = 1;
j.Memo = json.DocumentMemo;
j.NameAddressId = json.NameAddressId;
Database.Update(j);
j = new Journal();
j.AccountId = json.AccountId;
j.Outstanding = j.Amount = -json.Amount;
j.DocumentId = (int)json.idDocument;
j.JournalNum = 2;
j.Memo = json.DocumentMemo;
j.NameAddressId = json.NameAddressId;
Database.Update(j);
Line line = Database.Get<Line>((int)j.idJournal);
line.idLine = j.idJournal;
line.LineAmount = Math.Abs(json.Amount);
Database.Update(line);
JObject full = getCompleteDocument(json.idDocument);
Database.AuditUpdate("Document", json.idDocument, old, full);
Database.Commit();
return new AjaxReturn() { message = "Balance adjusted", id = json.idDocument };
}
public void Securities() {
}
public object SecuritiesListing() {
return Database.Query("SELECT * FROM Security ORDER BY SecurityName");
}
/// <summary>
/// Security header and stock prices
/// </summary>
public void Security(int id) {
Security record = Database.Get<Security>(id);
if (record.Id != null)
Title += " - " + record.SecurityName;
Record = new JObject().AddRange(
"header", record,
"detail", Database.Query("SELECT *, 7 AS Unit FROM StockPrice WHERE SecurityId = " + id + " ORDER BY Date DESC"));
}
public AjaxReturn SecuritySave(SecurityInfo json) {
Security existing = Database.Get(json.header);
Database.BeginTransaction();
Database.Update(json.header, true);
if (existing.idSecurity > 0 && json.header.SecurityName != existing.SecurityName) {
// Name has changed - change name of subaccounts
foreach(Account a in Database.Query<Account>("SELECT * FROM Account WHERE AccountName LIKE "
+ Database.Quote("%:" + existing.SecurityName))) {
if(a.AccountName.EndsWith(":" + existing.SecurityName)) {
a.AccountName = a.AccountName.Substring(0, a.AccountName.Length - existing.SecurityName.Length) + json.header.SecurityName;
Database.Update(a);
}
}
}
// Replace old stock prices with new ones
Database.Execute("DELETE FROM StockPrice WHERE SecurityId = " + json.header.idSecurity);
foreach (StockPrice p in json.detail) {
Database.Insert(p);
}
Database.Commit();
return new AjaxReturn() { message = "Security updated" };
}
/// <summary>
/// Sql to return the price of each stock as at date
/// </summary>
public static string LatestPrice(Database db, DateTime date) {
return string.Format(@"(select StockPrice.* FROM StockPrice JOIN
(select SecurityId AS Id, MAX(Date) AS MaxDate
FROM StockPrice
WHERE Date <= {0}
GROUP BY SecurityId) AS LatestPriceDate ON Id = SecurityId AND MaxDate = Date) AS LatestPrice", db.Quote(date));
}
/// <summary>
/// Calculate the cost of all the securities in an account on a FIFO basis
/// </summary>
public decimal SecurityCost(int account) {
decimal cost = 0;
List<CostedStockTransaction> transactions = Database.Query<CostedStockTransaction>(@"SELECT StockTransaction.*
FROM StockTransaction
JOIN Journal ON Journal.idJournal = idStockTransaction
JOIN Document ON idDocument = Journal.DocumentId
WHERE AccountId = " + account + @"
ORDER BY DocumentDate, idDocument").ToList();
foreach (CostedStockTransaction t in transactions) {
if (t.Quantity < 0) {
// Sold - work out the cost of the sold items
double q = -Math.Round(t.Quantity, 4); // Quantity sold
foreach (CostedStockTransaction b in transactions) {
if (b.Quantity > 0 && b.SecurityId == t.SecurityId) {
// Quantity bought
double qb = Math.Round(b.Quantity, 4);
// Amount sold, or quantity bought, whichever is the lesser
double qs = Math.Min(qb, q);
// Quantity left on purchase transaction
b.Quantity = Math.Round(qb - qs, 4);
q = Math.Round(q - qs, 4);
cost -= (decimal)Math.Round(b.CostPer * qs, 4);
if (q == 0)
break;
}
}
} else {
cost += (decimal)Math.Round(t.CostPer * t.Quantity, 4);
}
}
return Math.Round(cost, 2);
}
/// <summary>
/// Sql to return the current value of each StockTransaction as at Date
/// </summary>
public static string SecurityValues(Database db, DateTime date) {
return @"SELECT ParentAccountId, AccountId, SecuritiesByAccount.SecurityId AS SecurityId, Quantity, Price, SUM(ROUND(Quantity * Price, 2)) AS Value
FROM (SELECT DocumentDate, ParentAccountId, AccountId, SecurityId, SUM(Quantity) AS Quantity
FROM StockTransaction
JOIN Journal ON idJournal = idStockTransaction
JOIN Document ON idDocument = DocumentId
WHERE DocumentDate <= " + db.Quote(date) + @"
GROUP BY ParentAccountId, AccountId, SecurityId) AS SecuritiesByAccount
JOIN " + LatestPrice(db, date) + @"
ON LatestPrice.SecurityId = SecuritiesByAccount.SecurityId
GROUP BY AccountId, SecuritiesByAccount.SecurityId";
}
/// <summary>
/// Sql to return the current value of each security account as at fate
/// </summary>
public static string AccountValue(Database db, DateTime date) {
return @"SELECT ParentAccountId, SUM(Value) AS Value
FROM (" + SecurityValues(db, date) + @") AS SecurityValues
GROUP BY ParentAccountId";
}
public class InvestmentDetail : Account {
public decimal? CashBalance;
public decimal? CurrentBalance;
public decimal? Value;
}
public class SecurityValue : JsonObject {
public int? ParentAccountId;
public int? AccountId;
public int? SecurityId;
public decimal Value;
}
public class SecurityValueWithName : JsonObject {
public int? ParentAccountId;
public int? AccountId;
public int? SecurityId;
public string SecurityName;
public decimal Value;
}
public class CostedStockTransaction : StockTransaction {
public decimal Cost;
}
public class InvestmentDocument : Extended_Document {
public int? SecurityId;
public string SecurityName = "";
public double Quantity;
public double Price;
public int? FeeAccount;
public decimal Fee;
public string FeeMemo = "Fees";
}
public class BalanceAdjustmentDocument : Document {
public int DocumentAccountId;
public int AccountId;
public string AccountName;
public int? NameAddressId;
public string Name;
public decimal Amount;
public decimal ExistingBalance;
public decimal NewBalance;
}
public class SecurityInfo : JsonObject {
public Security header;
public List<StockPrice> detail;
}
}
}