A single entry on a LedgerColumn for a date (which comes from the LedgerEntryLine). This instance can contain one or more LedgerTransactions defining all movements for this BudgetBucket for this date. Possible transactions include budgeted 'saved up for expenses' credited into this LedgerColumn and all statement transactions that are debitted to this budget bucket ledger.
        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);
        }
示例#11
0
        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
     };
 }