private void SerializeTransactionBody( XmlWriter xmlWriter, Transaction trans ) { xmlWriter.WriteElementString( "Date", trans.Date.ToShortDateString() ); xmlWriter.WriteWhitespace( Environment.NewLine ); xmlWriter.WriteElementString( "Amount", trans.Amount.ToString() ); xmlWriter.WriteWhitespace( Environment.NewLine ); if( trans.Description != null && trans.Description.Length > 0 ) { xmlWriter.WriteElementString( "Description", trans.Description ); xmlWriter.WriteWhitespace( Environment.NewLine ); } /* // TODO: Remove Transaction.Number if( trans.HasNumber ) { xmlWriter.WriteElementString( "Number", trans.Number ); xmlWriter.WriteWhitespace( Environment.NewLine ); } */ if( trans.Memo != null && trans.Memo.Length > 0 ) { xmlWriter.WriteElementString( "Memo", trans.Memo ); xmlWriter.WriteWhitespace( Environment.NewLine ); } bool canUseShortcut = false; LineItem[] credits = trans.GetCreditLineItems(); LineItem[] debits = trans.GetDebitLineItems(); /* if( credits.Length != 1 || debits.Length != 1 ) canUseShortcut = false; else { if( credits[0].IsReconciled ) canUseShortcut = false; if( debits[0].IsReconciled ) canUseShortcut = false; if( debits[0].Account.AccountType != AccountType.Expense ) canUseShortcut = false; } */ if( canUseShortcut ) { xmlWriter.WriteElementString( "AccountId", credits[0].Account.Id.ToString() ); xmlWriter.WriteWhitespace( Environment.NewLine ); if( debits[0].CategoryObject != null ) { xmlWriter.WriteElementString( "Category", debits[0].CategoryObject.Name ); xmlWriter.WriteWhitespace( Environment.NewLine ); } } else { xmlWriter.WriteStartElement( "LineItems" ); xmlWriter.WriteWhitespace( Environment.NewLine ); foreach( LineItem lineItem in trans.LineItems ) { // <Credit> or <Debit> xmlWriter.WriteStartElement( lineItem.TransactionType.ToString() ); // Line items now have unique identifiers, too. if( lineItem.Id > 0 ) xmlWriter.WriteAttributeString( "Id", lineItem.Id.ToString() ); xmlWriter.WriteAttributeString( "AccountId", lineItem.Account.Id.ToString() ); // Number is new in 1.2 format if( lineItem.Number != null && lineItem.Number.Length > 0 ) xmlWriter.WriteAttributeString( "Number", lineItem.Number ); // Memo became an attribute in 1.2 format if( lineItem.CategoryObject != null ) xmlWriter.WriteAttributeString( "Category", lineItem.CategoryObject.Name ); if( lineItem.Memo != null && lineItem.Memo != trans.Memo ) xmlWriter.WriteAttributeString( "Memo", lineItem.Memo ); if( lineItem.Amount != trans.Amount ) xmlWriter.WriteAttributeString( "Amount", lineItem.Amount.ToString() ); if( lineItem.IsReconciled ) xmlWriter.WriteAttributeString( "Reconciled", "true" ); // New: Write the associated online transaction for this line item. if( lineItem.OnlineTransaction != null ) { xmlWriter.WriteWhitespace( Environment.NewLine ); OnlineTransactionStorageXml ostorage = new OnlineTransactionStorageXml(); ostorage.SerializeTransaction( xmlWriter, lineItem.OnlineTransaction ); xmlWriter.WriteWhitespace( Environment.NewLine ); } xmlWriter.WriteEndElement(); // </Credit> or </Debit> xmlWriter.WriteWhitespace( Environment.NewLine ); } xmlWriter.WriteEndElement(); // </LineItems> xmlWriter.WriteWhitespace( Environment.NewLine ); } }
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; }
public void SaveLedger( Ledger ledger ) { ledger.ValidateReferentialIntegrity(); // Ensure that identifiers match appropriately before saving. ValidateIdentifierLinks( ledger ); /* // TODO: Remove this after testing ledger.RecurringTransactions.Clear(); RecurringTransaction r = new RecurringTransaction(); r.StartingDate = new DateTime( 2007, 10, 15 ); r.EndingDate = DateTime.MaxValue; r.Date = new DateTime( 2007, 10, 15 ); r.Amount = 129.69M; r.Description = "Ameriprise Financial"; r.Memo = "Life insurance payment"; r.LineItems.Add( new LineItemCredit( ledger.GetAccount( "Suntrust" ), r.Amount ) ); r.LineItems.Add( new LineItemDebit( ledger.GetAccount( "Expenses" ), r.Amount ) ); r.AddMonths = 1; ledger.RecurringTransactions.Add( r ); */ StreamWriter writer = File.CreateText( filename ); try { XmlTextWriter xmlWriter = new XmlTextWriter( writer ); try { xmlWriter.WriteStartDocument(); xmlWriter.WriteWhitespace( Environment.NewLine ); xmlWriter.WriteStartElement( "Ledger" ); xmlWriter.WriteAttributeString( "xmlns", "http://www.uvteksoftware.com/xsd/UvMoneyLedger.xsd" ); xmlWriter.WriteAttributeString( "Version", "1.2" ); xmlWriter.WriteWhitespace( Environment.NewLine ); xmlWriter.WriteElementString( "MergeDate", ledger.MergeDate.ToString() ); xmlWriter.WriteWhitespace( Environment.NewLine ); // Write accounts xmlWriter.WriteWhitespace( Environment.NewLine ); xmlWriter.WriteStartElement( "Accounts" ); xmlWriter.WriteWhitespace( Environment.NewLine ); foreach( Account account in ledger.Accounts ) { SerializeAccount( xmlWriter, account ); xmlWriter.WriteWhitespace( Environment.NewLine ); } xmlWriter.WriteEndElement(); // </Accounts> xmlWriter.WriteWhitespace( Environment.NewLine ); // Write recurring transactions if( ledger.RecurringTransactions.Count > 0 ) { xmlWriter.WriteWhitespace( Environment.NewLine ); xmlWriter.WriteStartElement( "RecurringTransactions" ); xmlWriter.WriteWhitespace( Environment.NewLine ); foreach( RecurringTransaction retrans in ledger.RecurringTransactions ) { SerializeRecurringTransaction( xmlWriter, retrans ); xmlWriter.WriteWhitespace( Environment.NewLine ); } xmlWriter.WriteEndElement(); // </RecurringTransactions> xmlWriter.WriteWhitespace( Environment.NewLine ); } // Write transactions xmlWriter.WriteWhitespace( Environment.NewLine ); xmlWriter.WriteStartElement( "Transactions" ); xmlWriter.WriteWhitespace( Environment.NewLine ); foreach( Transaction trans in ledger.Transactions ) { // Do not serialize auto-generated transactions if( !( trans is MissingTransaction ) ) { trans.Validate(); SerializeTransaction( xmlWriter, trans ); xmlWriter.WriteWhitespace( Environment.NewLine ); } } xmlWriter.WriteWhitespace( Environment.NewLine ); xmlWriter.WriteComment( "Insert new transactions here." ); xmlWriter.WriteWhitespace( Environment.NewLine ); // Write out a transaction template StringBuilder sb = new StringBuilder(); sb.Append( Environment.NewLine ); sb.Append( "<Transaction>" ); sb.Append( Environment.NewLine ); sb.Append( "<Date></Date>" ); sb.Append( Environment.NewLine ); sb.Append( "<Number></Number>" ); sb.Append( Environment.NewLine ); sb.Append( "<Amount>0.00</Amount>" ); sb.Append( Environment.NewLine ); sb.Append( "<Description>Payee</Description>" ); sb.Append( Environment.NewLine ); sb.Append( "<Memo></Memo>" ); sb.Append( Environment.NewLine ); sb.Append( "<LineItems>" ); sb.Append( Environment.NewLine ); sb.Append( "<Credit Account=\"\" />" ); sb.Append( Environment.NewLine ); sb.Append( "<Debit Account=\"Expenses\" Category=\"\" />" ); sb.Append( Environment.NewLine ); sb.Append( "</LineItems>" ); sb.Append( Environment.NewLine ); sb.Append( "</Transaction>" ); sb.Append( Environment.NewLine ); xmlWriter.WriteComment( sb.ToString() ); xmlWriter.WriteWhitespace( Environment.NewLine ); xmlWriter.WriteWhitespace( Environment.NewLine ); xmlWriter.WriteEndElement(); // </Transactions> xmlWriter.WriteWhitespace( Environment.NewLine ); OnlineTransactionStorageXml ostorage = new OnlineTransactionStorageXml(); // Write online transactions that are missing from the ledger if( ledger.MissingTransactions.Count > 0 ) { xmlWriter.WriteStartElement( "MissingOnlineTransactions" ); xmlWriter.WriteWhitespace( Environment.NewLine ); foreach( OnlineTransaction otrans in ledger.MissingTransactions ) { ostorage.SerializeTransaction( xmlWriter, otrans ); xmlWriter.WriteWhitespace( Environment.NewLine ); } xmlWriter.WriteEndElement(); // </MissingOnlineTransactions> xmlWriter.WriteWhitespace( Environment.NewLine ); } xmlWriter.WriteEndElement(); // </Ledger> xmlWriter.WriteWhitespace( Environment.NewLine ); xmlWriter.WriteEndDocument(); } finally { xmlWriter.Close(); } } finally { writer.Close(); } }
/// <summary> /// Deserialize from an XML reader into a Transaction object. /// </summary> /// <param name="xmlReader">XmlReader to deserialize from.</param> /// <param name="ledger">Parent Ledger object.</param> /// <returns>A Transaction object.</returns> public Transaction DeserializeTransaction( XmlReader xmlReader, Version version, Ledger ledger ) { if( xmlReader.NodeType != XmlNodeType.Element || xmlReader.Name != "Transaction" ) throw new ApplicationException( "Needs a Transaction node to deserialize an transaction object." ); OnlineTransactionStorageXml ostorage = new OnlineTransactionStorageXml(); Transaction trans = new Transaction(); if( xmlReader.GetAttribute( "Id" ) != null ) trans.Id = Int32.Parse( xmlReader.GetAttribute( "Id" ) ); string voidAttribute = xmlReader.GetAttribute( "Void" ); string pendingAttribute = xmlReader.GetAttribute( "Pending" ); //string reconciledAttribute = xmlReader.GetAttribute( "Reconciled" ); string recurringAttribute = xmlReader.GetAttribute( "Recurring" ); // What's the best way to do a case-insensitive string compare? // Options: // x.Equals( y, StringComparison.OrdinalIgnoreCase) // x.ToLower() == y.ToLower() // string.Compare( x, y, true ); // string.Compare( x, y, StringComparison.OrdinalIgnoreCase ); if( string.Compare( voidAttribute, "true", StringComparison.OrdinalIgnoreCase ) == 0 ) trans.Flags |= TransactionFlags.Void; if( string.Compare( pendingAttribute, "true", StringComparison.OrdinalIgnoreCase ) == 0 ) trans.Flags |= TransactionFlags.Pending; //if( string.Compare( reconciledAttribute, "true", StringComparison.OrdinalIgnoreCase ) == 0 ) // trans.Flags |= TransactionFlags.Reconciled; if( string.Compare( recurringAttribute, "true", StringComparison.OrdinalIgnoreCase ) == 0 ) trans.Flags |= TransactionFlags.Recurring; bool accountShortcut = false; LineItem lineItem = null; while( xmlReader.Read() ) { string accountName = null; Account account = null; string categoryName = null; Category category = null; int accountId = 0; decimal amount = 0; string memo = null; switch( xmlReader.NodeType ) { case XmlNodeType.Element: switch( xmlReader.Name ) { case "Date": trans.Date = DateTime.Parse( xmlReader.ReadString() ); break; case "Amount": trans.Amount = Decimal.Parse( xmlReader.ReadString() ); break; case "Description": trans.Description = xmlReader.ReadString(); break; case "Memo": trans.Memo = xmlReader.ReadString(); break; case "Account": // Shortcut for LineItems accountShortcut = true; accountName = xmlReader.ReadString(); account = ledger.FindAccount( accountName ); if( account == null ) throw new ArgumentException( "Reference to undefined account named " + accountName ); lineItem = new LineItemCredit( account, trans.Amount ); lineItem.Transaction = trans; trans.LineItems.Add( lineItem ); break; case "AccountId": // Shortcut for LineItems accountShortcut = true; accountId = Int32.Parse( xmlReader.ReadString() ); account = ledger.FindAccount( accountId ); if( account == null ) throw new ArgumentException( "Reference to unknown account Id " + accountId ); lineItem = new LineItemCredit( account, trans.Amount ); trans.LineItems.Add( lineItem ); account = ledger.FindAccount( "Expenses" ); if( account == null ) throw new ArgumentException( "Expense account not defined." ); lineItem = new LineItemDebit( account, trans.Amount ); lineItem.Transaction = trans; trans.LineItems.Add( lineItem ); break; case "Category": // Shortcut for LineItems if( trans.LineItems.Count == 0 ) throw new ArgumentException( "Account or AccountId must be specified before Category element." ); LineItem[] debits = trans.GetDebitLineItems(); if( debits.Length != 1 ) throw new ArgumentException( "Category element can only be used when there is exactly one debit line item." ); account = ledger.FindAccount( "Expenses" ); categoryName = xmlReader.ReadString(); category = account.GetCategory( categoryName ); debits[0].CategoryObject = category; break; case "Credit": if( accountShortcut ) throw new ArgumentException( "LineItems not expected when using Account/Category elements." ); if( version == new Version( 1, 0 ) ) { throw new ApplicationException( "Version 1.0 data files are no longer supported." ); } else if( version >= new Version( 1, 1 ) ) { if( xmlReader.GetAttribute( "AccountId" ) != null ) { accountId = Int32.Parse( xmlReader.GetAttribute( "AccountId" ) ); account = ledger.FindAccount( accountId ); } else { account = ledger.FindAccount( xmlReader.GetAttribute( "Account" ) ); if( account == null ) throw new ArgumentException( "Reference to undefined account named " + xmlReader.GetAttribute( "Account" ) ); accountId = account.Id; } if( xmlReader.GetAttribute( "Category" ) != null ) category = account.GetCategory( xmlReader.GetAttribute( "Category" ) ); if( xmlReader.GetAttribute( "Amount" ) != null ) amount = Decimal.Parse( xmlReader.GetAttribute( "Amount" ) ); else amount = trans.Amount; if( version >= new Version( 1, 2 ) ) memo = xmlReader.GetAttribute( "Memo" ); else memo = xmlReader.ReadString(); } lineItem = new LineItemCredit( account, amount, category ); lineItem.Transaction = trans; lineItem.Memo = memo; lineItem.Number = xmlReader.GetAttribute( "Number" ); // could be null if( xmlReader.GetAttribute( "Reconciled" ) != null ) lineItem.IsReconciled = (xmlReader.GetAttribute( "Reconciled" ).ToLower() == "true"); if( xmlReader.GetAttribute( "Id" ) != null ) lineItem.Id = int.Parse( xmlReader.GetAttribute( "Id" ) ); trans.LineItems.Add( lineItem ); break; case "Debit": if( accountShortcut ) throw new ArgumentException( "LineItems not expected when using Account/Category elements." ); if( version == new Version( 1, 0 ) ) { throw new ApplicationException( "Version 1.0 data files are no longer supported." ); } else if( version >= new Version( 1, 1 ) ) { if( xmlReader.GetAttribute( "AccountId" ) != null ) { accountId = Int32.Parse( xmlReader.GetAttribute( "AccountId" ) ); account = ledger.FindAccount( accountId ); } else { account = ledger.FindAccount( xmlReader.GetAttribute( "Account" ) ); if( account == null ) throw new ArgumentException( "Reference to undefined account named " + xmlReader.GetAttribute( "Account" ) ); accountId = account.Id; } if( xmlReader.GetAttribute( "Category" ) != null ) category = account.GetCategory( xmlReader.GetAttribute( "Category" ) ); if( xmlReader.GetAttribute( "Amount" ) != null ) amount = Decimal.Parse( xmlReader.GetAttribute( "Amount" ) ); else amount = trans.Amount; if( version >= new Version( 1, 2 ) ) memo = xmlReader.GetAttribute( "Memo" ); else memo = xmlReader.ReadString(); } lineItem = new LineItemDebit( account, amount, category ); lineItem.Transaction = trans; lineItem.Memo = memo; lineItem.Number = xmlReader.GetAttribute( "Number" ); // could be null if( xmlReader.GetAttribute( "Reconciled" ) != null ) lineItem.IsReconciled = (xmlReader.GetAttribute( "Reconciled" ).ToLower() == "true"); if( xmlReader.GetAttribute( "Id" ) != null ) lineItem.Id = int.Parse( xmlReader.GetAttribute( "Id" ) ); trans.LineItems.Add( lineItem ); break; case "OnlineTransaction": // We need to figure out whether this is a child of // the Transaction element or a Credit/Debit element. // Try examining xmlReader.Depth. OnlineTransaction otrans = ostorage.DeserializeTransaction( xmlReader, version ); if( xmlReader.Depth > 3 ) { // Associate with LineItem lineItem.OnlineTransaction = otrans; } else { throw new StorageException( "OnlineTransaction nodes are no longer supported at the Transaction level." ); // Associate with Transaction (deprecated) // trans.OnlineTransactions.Add( otrans ); } break; default: break; } break; case XmlNodeType.EndElement: if( xmlReader.Name == "Transaction" ) return trans; break; } } trans.SetUnmodified(); return trans; }