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 ); } }
/// <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; }