public static IEnumerable<LedgerTransaction> FindAutoMatchingTransactions(LedgerEntry ledgerEntry, bool includeMatchedTransactions = false) { if (ledgerEntry == null) { return new List<LedgerTransaction>(); } if (includeMatchedTransactions) { return ledgerEntry.Transactions.Where(t => !string.IsNullOrWhiteSpace(t.AutoMatchingReference)); } return ledgerEntry.Transactions.Where( t => t.AutoMatchingReference.IsSomething() && !t.AutoMatchingReference.StartsWith(MatchedPrefix, StringComparison.Ordinal)); }
public void TestInitialise() { this.reconciliationDate = new DateTime(2013, 9, 20); this.subject = new LedgerEntry(true) { LedgerBucket = LedgerBookTestData.PowerLedger, Balance = OpeningBalance }; }
/// <summary> /// Show the Ledger Transactions view for viewing and editing Ledger Transactions. /// </summary> /// <param name="ledgerEntry"></param> /// <param name="isNew"></param> public void ShowDialog(LedgerEntry ledgerEntry, bool isNew) { if (ledgerEntry == null) { return; } LedgerEntry = ledgerEntry; ShownTransactions = new ObservableCollection<LedgerTransaction>(LedgerEntry.Transactions); Title = string.Format(CultureInfo.CurrentCulture, "{0} Transactions", ledgerEntry.LedgerColumn.BudgetBucket.Code); ShowDialogCommon(isNew); }
private static IEnumerable<LedgerTransaction> IncludeStatementTransactions(LedgerEntry newEntry, ICollection<Transaction> filteredStatementTransactions) { if (filteredStatementTransactions.None()) { return new List<LedgerTransaction>(); } List<Transaction> transactions = filteredStatementTransactions.Where(t => t.BudgetBucket == newEntry.LedgerBucket.BudgetBucket).ToList(); if (transactions.Any()) { IEnumerable<LedgerTransaction> newLedgerTransactions = transactions.Select( t => { if (t.Amount < 0) { return new CreditLedgerTransaction(t.Id) { Amount = t.Amount, Narrative = ExtractNarrative(t), Date = t.Date }; } return new CreditLedgerTransaction(t.Id) { Amount = t.Amount, Narrative = ExtractNarrative(t), Date = t.Date }; }); return newLedgerTransactions.ToList(); } return new List<LedgerTransaction>(); }
/// <summary> /// This is effectively stage 2 of the Reconciliation process. /// Called by <see cref="ReconciliationBuilder.CreateNewMonthlyReconciliation" />. It builds the contents of the new /// ledger line based on budget and /// statement input. /// </summary> /// <param name="budget">The current applicable budget</param> /// <param name="statement">The current period statement.</param> /// <param name="startDateIncl"> /// The date of the previous ledger line. This is used to include transactions from the /// Statement starting from this date and including this date. /// </param> private void AddNew( BudgetModel budget, StatementModel statement, DateTime startDateIncl) { if (!this.newReconciliationLine.IsNew) { throw new InvalidOperationException( "Cannot add a new entry to an existing Ledger Line, only new Ledger Lines can have new entries added."); } var reconciliationDate = this.newReconciliationLine.Date; // Date filter must include the start date, which goes back to and includes the previous ledger date up to the date of this ledger line, but excludes this ledger date. // For example if this is a reconciliation for the 20/Feb then the start date is 20/Jan and the finish date is 20/Feb. So transactions pulled from statement are between // 20/Jan (inclusive) and 19/Feb (inclusive). List<Transaction> filteredStatementTransactions = statement?.AllTransactions.Where( t => t.Date >= startDateIncl && t.Date < reconciliationDate).ToList() ?? new List<Transaction>(); IEnumerable<LedgerEntry> previousLedgerBalances = CompileLedgersAndBalances(LedgerBook); var entries = new List<LedgerEntry>(); foreach (var previousLedgerEntry in previousLedgerBalances) { LedgerBucket ledgerBucket; var openingBalance = previousLedgerEntry.Balance; var currentLedger = LedgerBook.Ledgers.Single(l => l.BudgetBucket == previousLedgerEntry.LedgerBucket.BudgetBucket); if (previousLedgerEntry.LedgerBucket.StoredInAccount != currentLedger.StoredInAccount) { // Check to see if a ledger has been moved into a new default account since last reconciliation. ledgerBucket = currentLedger; } else { ledgerBucket = previousLedgerEntry.LedgerBucket; } var newEntry = new LedgerEntry(true) { Balance = openingBalance, LedgerBucket = ledgerBucket }; List<LedgerTransaction> transactions = IncludeBudgetedAmount(budget, ledgerBucket, reconciliationDate); transactions.AddRange(IncludeStatementTransactions(newEntry, filteredStatementTransactions)); AutoMatchTransactionsAlreadyInPreviousPeriod(filteredStatementTransactions, previousLedgerEntry, transactions); newEntry.SetTransactionsForReconciliation(transactions, reconciliationDate); entries.Add(newEntry); } this.newReconciliationLine.SetNewLedgerEntries(entries); CreateBalanceAdjustmentTasksIfRequired(); if (statement != null) { AddBalanceAdjustmentsForFutureTransactions(statement, reconciliationDate); } CreateTasksToTransferFundsIfPaidFromDifferentAccount(filteredStatementTransactions); }
/// <summary> /// Match statement transaction with special automatching references to Ledger transactions. /// </summary> private void AutoMatchTransactionsAlreadyInPreviousPeriod( List<Transaction> transactions, LedgerEntry previousLedgerEntry, List<LedgerTransaction> newLedgerTransactions) { List<LedgerTransaction> ledgerAutoMatchTransactions = FindAutoMatchingTransactions(previousLedgerEntry).ToList(); var checkMatchedTxns = new List<LedgerTransaction>(); var checkMatchCount = 0; foreach (var lastMonthLedgerTransaction in ledgerAutoMatchTransactions) { this.logger.LogInfo( l => l.Format( "Ledger Reconciliation - AutoMatching - Found {0} {1} ledger transaction that require matching.", ledgerAutoMatchTransactions.Count(), previousLedgerEntry.LedgerBucket.BudgetBucket.Code)); var ledgerTxn = lastMonthLedgerTransaction; foreach ( var matchingStatementTransaction in TransactionsToAutoMatch(transactions, lastMonthLedgerTransaction.AutoMatchingReference)) { this.logger.LogInfo( l => l.Format("Ledger Reconciliation - AutoMatching - Matched {0} ==> {1}", ledgerTxn, matchingStatementTransaction)); ledgerTxn.Id = matchingStatementTransaction.Id; if (!ledgerTxn.AutoMatchingReference.StartsWith(MatchedPrefix, StringComparison.Ordinal)) { // There will be two statement transactions but only one ledger transaction to match to. checkMatchCount++; ledgerTxn.AutoMatchingReference = string.Format(CultureInfo.InvariantCulture, "{0}{1}", MatchedPrefix, ledgerTxn.AutoMatchingReference); checkMatchedTxns.Add(ledgerTxn); } var duplicateTransaction = newLedgerTransactions.FirstOrDefault(t => t.Id == matchingStatementTransaction.Id); if (duplicateTransaction != null) { this.logger.LogInfo( l => l.Format( "Ledger Reconciliation - Removing Duplicate Ledger transaction after auto-matching: {0}", duplicateTransaction)); newLedgerTransactions.Remove(duplicateTransaction); } } } if (ledgerAutoMatchTransactions.Any() && ledgerAutoMatchTransactions.Count() != checkMatchCount) { this.logger.LogWarning( l => l.Format( "Ledger Reconciliation - WARNING {0} ledger transactions appear to be waiting to be automatched, but not statement transactions were found. {1}", ledgerAutoMatchTransactions.Count(), ledgerAutoMatchTransactions.First().AutoMatchingReference)); IEnumerable<LedgerTransaction> unmatchedTxns = ledgerAutoMatchTransactions.Except(checkMatchedTxns); foreach (var txn in unmatchedTxns) { this.toDoList.Add( new ToDoTask( string.Format( CultureInfo.CurrentCulture, "WARNING: Missing auto-match transaction. Transfer {0:C} with reference {1} Dated {2:d} to {3}. See log for more details.", txn.Amount, txn.AutoMatchingReference, this.newReconciliationLine.Date.AddDays(-1), previousLedgerEntry.LedgerBucket.StoredInAccount), true)); } } }
/// <summary> /// Called by <see cref="LedgerBook.Reconcile" />. It builds the contents of the new ledger line based on budget and /// statement input. /// </summary> /// <param name="previousEntries"> /// A collection of previous <see cref="LedgerEntry" />s to construct the running balance for /// the entries this line contains. /// </param> /// <param name="currentBudget">The current applicable budget</param> /// <param name="statement">The current period statement.</param> /// <param name="startDateIncl">The date for this ledger line.</param> internal void AddNew(IEnumerable<KeyValuePair<LedgerColumn, LedgerEntry>> previousEntries, BudgetModel currentBudget, StatementModel statement, DateTime startDateIncl) { if (!IsNew) { throw new InvalidOperationException("Cannot add a new entry to an existing Ledger Line, only new Ledger Lines can have new entries added."); } DateTime finishDateExcl = Date; List<Transaction> filteredStatementTransactions = statement == null ? new List<Transaction>() : statement.AllTransactions.Where(t => t.Date >= startDateIncl && t.Date < finishDateExcl).ToList(); foreach (var previousEntry in previousEntries) { LedgerColumn ledger = previousEntry.Key; decimal balance = previousEntry.Value == null ? 0 : previousEntry.Value.Balance; var newEntry = new LedgerEntry(true) { Balance = balance, LedgerColumn = ledger }; Expense expenseBudget = currentBudget.Expenses.FirstOrDefault(e => e.Bucket.Code == ledger.BudgetBucket.Code); var transactions = new List<LedgerTransaction>(); if (expenseBudget != null) { var budgetedAmount = new BudgetCreditLedgerTransaction { Credit = expenseBudget.Amount, Narrative = "Budgeted Amount" }; transactions.Add(budgetedAmount); } transactions.AddRange(IncludeStatementTransactions(newEntry, filteredStatementTransactions)); newEntry.SetTransactionsForReconciliation(transactions); this.entries.Add(newEntry); } }
private static IEnumerable<LedgerTransaction> IncludeStatementTransactions(LedgerEntry newEntry, ICollection<Transaction> filteredStatementTransactions) { if (!filteredStatementTransactions.Any()) { return new List<LedgerTransaction>(); } List<Transaction> transactions = filteredStatementTransactions.Where(t => t.BudgetBucket == newEntry.LedgerColumn.BudgetBucket).ToList(); if (transactions.Any()) { IEnumerable<LedgerTransaction> newLedgerTransactions = transactions.Select<Transaction, LedgerTransaction>( t => { if (t.Amount < 0) { return new DebitLedgerTransaction(t.Id) { BankAccount = t.AccountType, Debit = -t.Amount, // Statement debits are negative, I want them to be positive here unless they are debit reversals where they should be negative. Narrative = ExtractNarrative(t), }; } return new CreditLedgerTransaction(t.Id) { BankAccount = t.AccountType, Credit = t.Amount, Narrative = ExtractNarrative(t), }; }); return newLedgerTransactions.ToList(); } return new List<LedgerTransaction>(); }
public LedgerTransaction CreateLedgerTransaction(LedgerEntryLine reconciliation, LedgerEntry ledgerEntry, decimal amount, string narrative) { if (reconciliation == null) { throw new ArgumentNullException(nameof(reconciliation)); } if (ledgerEntry == null) { throw new ArgumentNullException(nameof(ledgerEntry)); } if (narrative == null) { throw new ArgumentNullException(nameof(narrative)); } LedgerTransaction newTransaction = new CreditLedgerTransaction(); newTransaction.WithAmount(amount).WithNarrative(narrative); newTransaction.Date = reconciliation.Date; ledgerEntry.AddTransaction(newTransaction); return newTransaction; }
public void RemoveTransaction(LedgerEntry ledgerEntry, Guid transactionId) { if (ledgerEntry == null) { throw new ArgumentNullException(nameof(ledgerEntry)); } ledgerEntry.RemoveTransaction(transactionId); }
private void SetReconciliation(IReadOnlyDictionary<LedgerBucket, SpecificLedgerEntryTestDataBuilder> ledgers, string remarks) { var recon = new LedgerEntryLine(this.tempReconDate, this.tempBankBalances) { Remarks = remarks }; LedgerEntryLine previousRecon = Reconciliations.OrderByDescending(r => r.Date).FirstOrDefault(); var entries = new List<LedgerEntry>(); foreach (LedgerBucket ledgerBucket in this.ledgerBuckets) { decimal openingBalance; if (previousRecon == null) { openingBalance = this.openingBalances[ledgerBucket]; } else { LedgerEntry previousEntry = previousRecon.Entries.Single(e => e.LedgerBucket == ledgerBucket); openingBalance = previousEntry.Balance; } var entry = new LedgerEntry { LedgerBucket = ledgerBucket, Balance = openingBalance }; entry.SetTransactionsForTesting(ledgers[ledgerBucket].Transactions.ToList()); entries.Add(entry); } recon.SetEntriesForTesting(entries); this.reconciliations.Add(recon); }
/// <summary> /// Show the Ledger Transactions view for viewing and editing Ledger Transactions. /// </summary> public void ShowLedgerTransactionsDialog(LedgerEntryLine ledgerEntryLine, LedgerEntry ledgerEntry, bool isNew) { if (ledgerEntry == null) { return; } InBalanceAdjustmentMode = false; InLedgerEntryMode = true; LedgerEntry = ledgerEntry; this.entryLine = ledgerEntryLine; // Will be null when editing an existing LedgerEntry as opposed to creating a new reconciliation. ShownTransactions = new ObservableCollection<LedgerTransaction>(LedgerEntry.Transactions); Title = string.Format(CultureInfo.CurrentCulture, "{0} Transactions", ledgerEntry.LedgerBucket.BudgetBucket.Code); OpeningBalance = RetrieveOpeningBalance(); ShowDialogCommon(isNew); }
public void TestInitialise() { this.subject = new LedgerEntry(true) { LedgerBucket = LedgerBookTestData.CarInsLedger, Balance = OpeningBalance }; }