private int LoadTransaction( StreamReader reader, int lineNumber, Ledger ledger, Account account ) { Transaction trans = new Transaction(); try { LineItem item; decimal transAmount = 0; string lineItemCategory = null; string lineItemMemo = null; decimal lineItemAmount = 0; string transCategory = null; bool isCleared = false; string checkNumber = null; string investmentName = null; string investmentPrice = null; string investmentShares = null; string investmentCommission = null; while( !reader.EndOfStream ) { string line = reader.ReadLine(); if( line[0] == '^' ) break; switch( line[0] ) { case 'D': // Date // MM/DD'YYYY // We handle a bunch of different separators // TODO: Handle DD/MM/YYYY European form ?? Regex regex = new Regex( "D(\\d+)['.-/]\\s*(\\d+)['.-/]\\s*(\\d+)" ); if( !regex.IsMatch( line ) ) throw new StorageException( "Cannot parse date " + line ); Match match = regex.Match( line ); int month = Int32.Parse( match.Groups[1].Value ); int day = Int32.Parse( match.Groups[2].Value ); int year = Int32.Parse( match.Groups[3].Value ); // Handle two-digit dates which are assumed to be 1900s if( year < 100 ) year += 1900; trans.Date = new DateTime( year, month, day ); break; case 'T': // Transaction amount (presumably dollars). // This is actually the amount that involves this account, // not necessarily the total transaction amount. // Transactions with splits may have a different total amount. // Particularly paychecks. // The transaction total is the sum of the credits. #region Split transaction sample // Split sample, as exported from MS Money: // // D5/23'2006 // CX // MPaycheck + cash out // T384.04 <-- (debit) not the total! // PEmmanuel Church // LWages & Salary:Gross Pay-Cyn // SWages & Salary:Gross Pay-Cyn // EGross Salary // $625.33 <-- (credit) // STaxes:Federal Income // EFederal // $-15.45 <-- (debit) // STaxes:Social Security // EFica // $-38.77 <-- (debit) // STaxes:Medicare Tax 20 // EMedicare // $-9.07 <-- (debit) // STaxes:State Income Ta // EState // $-18.00 <-- (debit) // SGroceries // ECash for groceries // $-150.00 <-- (debit) // SCash Withdrawal // ECash // $-50.00 <-- (debit) // SPiano Business // EStudents? // $40.00 <-- (credit) // ^ #endregion transAmount = Decimal.Parse( line.Substring( 1 ) ); trans.Amount = Math.Abs( transAmount ); break; case 'C': // Cleared status // CX = cleared if( line[1] == 'X' ) isCleared = true; break; case 'N': // Check number // If investment account, this is the action: buy, sell, etc. checkNumber = line.Substring( 1 ); break; case 'P': // Payee trans.Description = line.Substring( 1 ); break; case 'M': // Memo trans.Memo = line.Substring( 1 ); break; case 'L': // Category/Account transCategory = line.Substring( 1 ); break; case 'S': // Split line item category (like L above) // (If there are splits, it changes the meaning of the T amount.) lineItemCategory = line.Substring( 1 ); lineItemMemo = null; lineItemAmount = 0; break; case 'E': // Split line item memo (like M above) lineItemMemo = line.Substring( 1 ); break; case '$': // Split line item amount (like T above) lineItemAmount = Decimal.Parse( line.Substring( 1 ) ); item = BuildLineItem( ledger, trans, lineItemCategory, lineItemMemo, lineItemAmount ); item.IsReconciled = isCleared; trans.LineItems.Add( item ); break; // Investment indicators case 'Y': // security name investmentName = line.Substring( 1 ); break; case 'I': // price investmentPrice = line.Substring( 1 ); break; case 'Q': // quantity (shares) investmentShares = line.Substring( 1 ); break; case 'O': // commission investmentCommission = line.Substring( 1 ); break; default: // Unrecognized - ignore break; } lineNumber++; } // while if( trans.LineItems.Count > 0 ) { // Transaction had splits. // Add the transaction amount as the final split. item = BuildLineItem( ledger, trans, transCategory, trans.Memo, -transAmount ); item.Number = checkNumber; item.Account = account; item.CategoryObject = null; item.IsReconciled = isCleared; trans.LineItems.Add( item ); // Adjust total transaction amount. trans.Amount = trans.GetCreditLineItemSum(); } else { // No splits. Must create a matching debit and credit. Account otherAccount; Category otherCategory; otherAccount = ParseAccount( ledger, transCategory, transAmount, out otherCategory ); if( transAmount < 0 ) { // Withdrawal item = trans.CreateLineItem( TransactionType.Credit, account, Math.Abs( transAmount ), null ); item.Number = checkNumber; item.IsReconciled = isCleared; trans.LineItems.Add( item ); item = trans.CreateLineItem( TransactionType.Debit, otherAccount, Math.Abs( transAmount ), otherCategory ); trans.LineItems.Add( item ); } else { // Deposit item = trans.CreateLineItem( TransactionType.Credit, otherAccount, Math.Abs( transAmount ), otherCategory ); trans.LineItems.Add( item ); item = trans.CreateLineItem( TransactionType.Debit, account, Math.Abs( transAmount ), null ); item.IsReconciled = isCleared; trans.LineItems.Add( item ); } } trans.LineItems.Sort(); trans.Validate(); // Special case "Opening Balance" line: if( trans.Description == "Opening Balance" && account.GetLineItems().Count == 0 ) { // The account name is given in the category account.Name = transCategory.Trim( '[', ']' ); account.StartingBalance = account.IsLiability ? -transAmount : transAmount; account.StartingDate = trans.Date; } else { trans.Id = ledger.GetNextTransactionId(); ledger.AddTransaction( trans ); } } catch( Exception ex ) { // Don't add broken transactions Console.WriteLine( "Error on line number " + lineNumber.ToString() ); Console.WriteLine( ex.ToString() ); throw; } return lineNumber; }
public Ledger LoadLedger() { Ledger ledger = new Ledger(); FileInfo fi = new FileInfo( filename ); if( !fi.Exists ) return ledger; OnlineTransactionStorageXml ostorage = new OnlineTransactionStorageXml(); Version version = new Version( 1, 0 ); // I'm not precisely sure why I chose to parse the XML manually // instead of using XmlSerializer. // I guess I just didn't want to markup my classes with Xml tags. StreamReader reader = File.OpenText( filename ); try { XmlTextReader xmlReader = new XmlTextReader( reader ); try { while( xmlReader.Read() ) { switch( xmlReader.NodeType ) { case XmlNodeType.Element: switch( xmlReader.Name ) { case "MergeDate": ledger.MergeDate = DateTime.Parse( xmlReader.ReadString() ); break; case "Ledger": version = new Version( xmlReader.GetAttribute( "Version" ) ); break; case "Accounts": break; case "Account": Account account = DeserializeAccount( xmlReader, version ); account.Ledger = ledger; // Verify that we are not adding any duplicate Ids or Names Account testAccount = ledger.FindAccount( account.Id ); if( testAccount != null ) throw new ApplicationException( "There is already an account with Id " + account.Id ); testAccount = ledger.FindAccount( account.Name ); if( testAccount != null ) throw new ApplicationException( "There is already an account named " + account.Name ); // Passed validation, add the account ledger.Accounts.Add( account ); break; case "Transactions": break; case "Transaction": Transaction trans = DeserializeTransaction( xmlReader, version, ledger ); // Migrate from deprecated versions trans.Migrate( version ); // Validate the transaction trans.Validate(); if( trans.Id != 0 ) { // Ensure no other transactions with the same Id are in the ledger Transaction testTrans = ledger.GetTransaction( trans.Id ); if( testTrans != null ) throw new ApplicationException( "There is already a transaction with Id " + trans.Id ); } ledger.AddTransaction( trans ); break; case "MissingOnlineTransactions": break; case "OnlineTransaction": OnlineTransaction otrans = ostorage.DeserializeTransaction( xmlReader, version ); ledger.MissingTransactions.Add( otrans ); break; case "RecurringTransactions": break; case "RecurringTransaction": RecurringTransaction rtrans = DeserializeRecurringTransaction( xmlReader, version, ledger ); ledger.RecurringTransactions.Add( rtrans ); break; } break; } } } finally { xmlReader.Close(); } } finally { reader.Close(); } AssignTransactionIdentifiers( ledger ); AssignLineItemIdentifiers( ledger ); ValidateIdentifierLinks( ledger ); ledger.ValidateReferentialIntegrity(); // Set load date so we can determine if data in memory is stale ledger.LoadDate = DateTime.Now; return ledger; }
protected void SubmitPurchase_Click( object sender, EventArgs e ) { try { dataMgr.Lock(); try { // TODO: Remove obsolete DataManager code. // Make sure we have the very latest data file. ledger = dataMgr.Load( false, false ); // Build a transaction object Transaction trans = new Transaction(); trans.Id = ledger.GetNextTransactionId(); trans.Date = DateTime.Parse( txtDate.Text ); trans.Amount = Convert.ToDecimal( txtAmount.Text ); trans.Description = txtPayee.Text; if( txtMemo.Text.Length > 0 ) trans.Memo = txtMemo.Text; // Populate line items Account account = ledger.FindAccount( Convert.ToInt32( ddlAccount.SelectedValue ) ); LineItemCredit credit = new LineItemCredit( account, trans.Amount ); credit.Id = ledger.GetNextLineItemId(); credit.Transaction = trans; if( txtNumber.Text.Length > 0 ) credit.Number = txtNumber.Text; if( txtMemo.Text.Length > 0 ) credit.Memo = txtMemo.Text; trans.LineItems.Add( credit ); Account expenseAccount = null; Category expenseCategory = null; if( !ledger.ParseCategory( txtCategory.Text, out expenseAccount, out expenseCategory ) ) { SubmitPurchaseStatusLabel.Text = "The category provided could not be found."; return; } LineItemDebit debit = new LineItemDebit( expenseAccount, trans.Amount ); debit.Id = ledger.GetNextLineItemId(); debit.Transaction = trans; debit.CategoryObject = expenseCategory; trans.LineItems.Add( debit ); // Add to ledger and save ledger.AddTransaction( trans ); // HACK: Removing until we figure out how to fix this... // ledger.RefreshMissingTransactions(); ledger.SortTransactions(); dataMgr.Save( ledger ); decimal newBalance = account.Balance; SubmitPurchaseStatusLabel.Text = string.Format( "Transaction #{0} saved. {2} balance {1:N2}.", trans.Id, newBalance, account.Name ); #if DATASTORAGE try { // DataLedger is initialized in BasePage.OnInit DataLedger.Storage.BeginTransaction(); try { IAccount dbaccount = DataLedger.FindAccountByName( account.Name ); if( dbaccount == null ) throw new ApplicationException( "Account " + account.Name + " not found in data storage." ); IAccount dbexpense = DataLedger.FindAccountByName( expenseAccount.Name ); if( dbexpense == null ) throw new ApplicationException( "Expense account " + expenseAccount.Name + " not found in data storage." ); ITransaction dbtrans = DataLedger.Storage.CreateTransaction(); dbtrans.Amount = trans.Amount; dbtrans.Date = trans.Date; dbtrans.Memo = trans.Memo; dbtrans.Payee = trans.Payee; DataLedger.Storage.SaveTransaction( dbtrans ); ILineItem dbcredit = DataLedger.Storage.CreateLineItem( dbtrans ); dbcredit.AccountId = dbaccount.Id; dbcredit.Amount = credit.Amount; dbcredit.Category = null; dbcredit.Memo = credit.Memo; dbcredit.Number = credit.Number; dbcredit.Type = TransactionType.Credit; DataLedger.Storage.SaveLineItem( dbcredit ); ILineItem dbdebit = DataLedger.Storage.CreateLineItem( dbtrans ); dbdebit.AccountId = dbexpense.Id; dbdebit.Amount = debit.Amount; dbdebit.Category = debit.Category; dbdebit.Type = TransactionType.Debit; DataLedger.Storage.SaveLineItem( dbdebit ); DataLedger.Storage.CommitTransaction(); DataLedger.RefreshUnmatchedRecords(); } catch( Exception ) { DataLedger.Storage.RollbackTransaction(); throw; } } catch( Exception ex ) { SubmitPurchaseStatusLabel.Text += "<br />An error occurred while saving to data storage: <br /><pre>" + ex.ToString() + "</pre>"; } #endif // Reset text boxes txtNumber.Text = ""; txtAmount.Text = ""; txtPayee.Text = ""; txtMemo.Text = ""; txtCategory.Text = ""; } finally { dataMgr.Unlock(); } } catch( LockException ex ) { SubmitPurchaseStatusLabel.Text = "The data file is in use by '" + ex.LockOwner + "'. Please try again in a moment."; } catch( Exception ex ) { SubmitPurchaseStatusLabel.Text = "An error occurred while saving: " + ex.Message; } }