aboutsummaryrefslogtreecommitdiff
path: root/cashflow.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'cashflow.cpp')
-rw-r--r--cashflow.cpp399
1 files changed, 232 insertions, 167 deletions
diff --git a/cashflow.cpp b/cashflow.cpp
index f302110..e157a8f 100644
--- a/cashflow.cpp
+++ b/cashflow.cpp
@@ -2,6 +2,7 @@
#include "ui_cashflow.h"
#include "settingsdialog.h"
#include <QMessageBox>
+#include <QDebug>
#include <QDir>
#include <QStandardPaths>
#include <QFontDialog>
@@ -34,6 +35,10 @@ CashFlow::CashFlow(QWidget *parent)
QMessageBox::critical(this, "Database Error", "Failed to open default database: " + database->lastError());
return;
}
+
+ // Regenerate all projections on startup to ensure they're current
+ database->regenerateAllProjections();
+ refreshView(); // Refresh to show new projection IDs
}
CashFlow::~CashFlow()
@@ -70,6 +75,16 @@ void CashFlow::setupConnections() {
connect(ui->saveBtn, &QPushButton::clicked, this, &CashFlow::onSaveTransaction);
connect(ui->newBtn, &QPushButton::clicked, this, &CashFlow::onNewTransaction);
connect(ui->deleteBtn, &QPushButton::clicked, this, &CashFlow::onDeleteTransaction);
+ connect(ui->adjustmentBtn, &QPushButton::clicked, this, &CashFlow::onCreateAdjustment);
+
+ // Transaction type change - grey out fields for reconciliation
+ connect(ui->entryTypeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) {
+ bool isReconciliation = (index == 2); // Reconciliation is index 2
+ ui->entryCategoryCombo->setEnabled(!isReconciliation);
+ ui->entryRecurringCombo->setEnabled(!isReconciliation);
+ ui->entryOccurrenceEdit->setEnabled(!isReconciliation);
+ ui->adjustmentBtn->setVisible(false); // Hide until we load a reconciliation
+ });
// Transaction entry recurring rule linking
connect(ui->entryRecurringCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &CashFlow::onRecurringRuleChanged);
@@ -207,19 +222,24 @@ void CashFlow::refreshTransactionTable() {
// Store ID in first column's data for retrieval later
QTableWidgetItem *dateItem = new QTableWidgetItem(t.date.toString("MM/dd/yy"));
dateItem->setData(Qt::UserRole, t.id);
- // Store full transaction for projected items (id == -1)
- if (t.id == -1) {
- dateItem->setData(Qt::UserRole + 1, QVariant::fromValue(t));
- }
dateItem->setFlags(dateItem->flags() & ~Qt::ItemIsEditable);
ui->transactionTable->setItem(row, 0, dateItem);
+ // For reconciliation rows, show expected balance in amount column, variance in balance column
+ double displayAmount = t.amount;
+ double displayBalance = runningBalance;
+
+ if (t.type == TransactionType::Reconciliation) {
+ displayAmount = t.expectedBalance; // Show what bank says
+ displayBalance = t.expectedBalance - t.calculatedBalance; // Show variance
+ }
+
// Format amount with color, right-align, monospace
- QTableWidgetItem *amountItem = new QTableWidgetItem(QString("$%1").arg(formatCurrency(t.amount)));
+ QTableWidgetItem *amountItem = new QTableWidgetItem(QString("$%1").arg(formatCurrency(displayAmount)));
amountItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
amountItem->setFont(currentAmountFont);
amountItem->setFlags(amountItem->flags() & ~Qt::ItemIsEditable);
- if (t.amount < 0) {
+ if (displayAmount < 0) {
amountItem->setForeground(QColor(200, 0, 0));
} else {
amountItem->setForeground(QColor(0, 150, 0));
@@ -227,11 +247,11 @@ void CashFlow::refreshTransactionTable() {
ui->transactionTable->setItem(row, 1, amountItem);
// Format balance with right-align, monospace
- QTableWidgetItem *balanceItem = new QTableWidgetItem(QString("$%1").arg(formatCurrency(runningBalance)));
+ QTableWidgetItem *balanceItem = new QTableWidgetItem(QString("$%1").arg(formatCurrency(displayBalance)));
balanceItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
balanceItem->setFont(currentAmountFont);
balanceItem->setFlags(balanceItem->flags() & ~Qt::ItemIsEditable);
- if (runningBalance < 0) {
+ if (displayBalance < 0) {
balanceItem->setForeground(QColor(200, 0, 0));
}
ui->transactionTable->setItem(row, 2, balanceItem);
@@ -244,18 +264,35 @@ void CashFlow::refreshTransactionTable() {
categoryItem->setFlags(categoryItem->flags() & ~Qt::ItemIsEditable);
ui->transactionTable->setItem(row, 4, categoryItem);
- QTableWidgetItem *descItem = new QTableWidgetItem(t.description);
+ // For reconciliation, show calculated status in description
+ QString displayDescription = t.description;
+ if (t.type == TransactionType::Reconciliation) {
+ double variance = t.expectedBalance - t.calculatedBalance;
+ QString status = (qAbs(variance) < 0.01) ? "Balanced" : QString("Out of balance by $%1").arg(formatCurrency(variance));
+ displayDescription = t.description.isEmpty() ? status : QString("%1 - %2").arg(t.description, status);
+ }
+
+ QTableWidgetItem *descItem = new QTableWidgetItem(displayDescription);
descItem->setFlags(descItem->flags() & ~Qt::ItemIsEditable);
ui->transactionTable->setItem(row, 5, descItem);
- QTableWidgetItem *typeItem = new QTableWidgetItem(
- t.type == TransactionType::Actual ? "Actual" : "Estimated");
+ QString typeLabel = "Estimated";
+ if (t.type == TransactionType::Actual) typeLabel = "Actual";
+ else if (t.type == TransactionType::Reconciliation) typeLabel = "Reconciliation";
+
+ QTableWidgetItem *typeItem = new QTableWidgetItem(typeLabel);
typeItem->setFlags(typeItem->flags() & ~Qt::ItemIsEditable);
ui->transactionTable->setItem(row, 6, typeItem);
- // Color code estimated vs actual
- QColor rowColor = t.type == TransactionType::Actual ?
- QColor(200, 255, 200) : QColor(255, 255, 200);
+ // Color code: Actual=green, Estimated=yellow, Reconciliation=red if mismatch, green if balanced
+ QColor rowColor;
+ if (t.type == TransactionType::Reconciliation) {
+ double variance = qAbs(t.expectedBalance - t.calculatedBalance);
+ rowColor = (variance < 0.01) ? QColor(200, 255, 200) : QColor(255, 200, 200);
+ } else {
+ rowColor = t.type == TransactionType::Actual ? QColor(200, 255, 200) : QColor(255, 255, 200);
+ }
+
for (int col = 0; col < 7; col++) {
if (ui->transactionTable->item(row, col)) {
ui->transactionTable->item(row, col)->setBackground(rowColor);
@@ -456,14 +493,8 @@ QList<Transaction> CashFlow::getAllTransactionsInRange() {
accountFilter = "All Accounts";
}
- // Get actual transactions from database
- QList<Transaction> actualTransactions = database->getTransactions(startDate, endDate);
-
- // Generate projected transactions from recurring rules
- QList<Transaction> projectedTransactions = generateProjectedTransactions();
-
- // Combine
- QList<Transaction> allTransactions = actualTransactions + projectedTransactions;
+ // Get all transactions from database (includes both actual and projected)
+ QList<Transaction> allTransactions = database->getTransactions(startDate, endDate);
// Filter by account if not "All Accounts"
if (accountFilter != "All Accounts") {
@@ -476,10 +507,15 @@ QList<Transaction> CashFlow::getAllTransactionsInRange() {
allTransactions = filtered;
}
- // Sort by date, then by sort_order, then credits before debits
+ // Sort by date, then reconciliation always last, then by sort_order, then credits before debits
std::sort(allTransactions.begin(), allTransactions.end(),
[](const Transaction &a, const Transaction &b) {
if (a.date != b.date) return a.date < b.date;
+
+ // Reconciliation transactions always sort LAST on their date
+ if (a.type == TransactionType::Reconciliation && b.type != TransactionType::Reconciliation) return false;
+ if (a.type != TransactionType::Reconciliation && b.type == TransactionType::Reconciliation) return true;
+
if (a.sortOrder != b.sortOrder) return a.sortOrder < b.sortOrder;
// Credits (positive amounts) before debits (negative amounts)
return a.amount > b.amount;
@@ -488,116 +524,11 @@ QList<Transaction> CashFlow::getAllTransactionsInRange() {
return allTransactions;
}
-QList<Transaction> CashFlow::generateProjectedTransactions() {
- QList<Transaction> projections;
- QList<RecurringRule> rules = database->getAllRecurringRules();
-
- QDate startDate = ui->dateFromEdit->date();
- QDate endDate = ui->dateToEdit->date();
-
- // Load all actual transactions with recurring_id (converted projections)
- QList<Transaction> actualTransactions = database->getAllTransactions();
- QSet<QString> existingOccurrences; // recurring_id:occurrence_key pairs
- for (const Transaction &t : actualTransactions) {
- if (t.recurringId != -1 && !t.occurrenceKey.isEmpty()) {
- existingOccurrences.insert(QString("%1:%2").arg(t.recurringId).arg(t.occurrenceKey));
- }
- }
-
- for (const RecurringRule &rule : rules) {
- QDate currentDate = rule.startDate > startDate ? rule.startDate : startDate;
-
- // Align to proper day based on frequency
- if (rule.frequency == RecurrenceFrequency::Weekly || rule.frequency == RecurrenceFrequency::BiWeekly) {
- // Find next occurrence of the specified day of week
- while (currentDate <= endDate && currentDate.dayOfWeek() != rule.dayOfWeek) {
- currentDate = currentDate.addDays(1);
- }
- } else if (rule.frequency == RecurrenceFrequency::Monthly) {
- // Set to the specified day of month
- int targetDay = qMin(rule.dayOfMonth, currentDate.daysInMonth());
- currentDate = QDate(currentDate.year(), currentDate.month(), targetDay);
- if (currentDate < startDate) {
- currentDate = currentDate.addMonths(1);
- targetDay = qMin(rule.dayOfMonth, currentDate.daysInMonth());
- currentDate = QDate(currentDate.year(), currentDate.month(), targetDay);
- }
- }
-
- int count = 0;
- while (currentDate <= endDate) {
- if (rule.occurrences != -1 && count >= rule.occurrences) {
- break;
- }
-
- if (rule.endDate.isValid() && currentDate > rule.endDate) {
- break;
- }
-
- // Generate occurrence key based on frequency
- QString occurrenceKey;
- if (rule.frequency == RecurrenceFrequency::Daily) {
- occurrenceKey = currentDate.toString("yyyy-MM-dd");
- } else if (rule.frequency == RecurrenceFrequency::Weekly || rule.frequency == RecurrenceFrequency::BiWeekly) {
- occurrenceKey = QString("%1-W%2").arg(currentDate.year()).arg(currentDate.weekNumber(), 2, 10, QChar('0'));
- } else if (rule.frequency == RecurrenceFrequency::Monthly) {
- occurrenceKey = currentDate.toString("yyyy-MM");
- } else if (rule.frequency == RecurrenceFrequency::Yearly) {
- occurrenceKey = QString::number(currentDate.year());
- }
-
- // Skip if actual already exists for this occurrence
- QString occurrenceCheck = QString("%1:%2").arg(rule.id).arg(occurrenceKey);
- if (existingOccurrences.contains(occurrenceCheck)) {
- // Actual exists, skip this projection
- } else {
- Transaction t;
- t.id = -1; // Projected transactions have no ID
- t.date = currentDate;
- t.amount = rule.amount;
- t.account = rule.account;
- t.category = rule.category;
- t.description = rule.description + " (projected)";
- t.type = TransactionType::Estimated;
- t.recurringId = rule.id;
- t.occurrenceKey = occurrenceKey;
- t.reconciled = false;
-
- projections.append(t);
- }
- count++;
-
- // Calculate next occurrence
- switch (rule.frequency) {
- case RecurrenceFrequency::Daily:
- currentDate = currentDate.addDays(1);
- break;
- case RecurrenceFrequency::Weekly:
- currentDate = currentDate.addDays(7);
- break;
- case RecurrenceFrequency::BiWeekly:
- currentDate = currentDate.addDays(14);
- break;
- case RecurrenceFrequency::Monthly: {
- currentDate = currentDate.addMonths(1);
- int targetDay = qMin(rule.dayOfMonth, currentDate.daysInMonth());
- currentDate = QDate(currentDate.year(), currentDate.month(), targetDay);
- break;
- }
- case RecurrenceFrequency::Yearly:
- currentDate = currentDate.addYears(1);
- break;
- default:
- currentDate = endDate.addDays(1); // Exit loop
- break;
- }
- }
- }
-
- return projections;
-}
-
void CashFlow::onDateRangeChanged() {
+ // Save date range to settings
+ database->setSetting("date_from", ui->dateFromEdit->date().toString(Qt::ISODate));
+ database->setSetting("date_to", ui->dateToEdit->date().toString(Qt::ISODate));
+
refreshView();
}
@@ -614,26 +545,14 @@ void CashFlow::onTransactionSelected() {
int row = selected[0]->row();
int id = ui->transactionTable->item(row, 0)->data(Qt::UserRole).toInt();
- // If it's a projected transaction (id = -1), load it for editing
- if (id == -1) {
- QVariant projectedData = ui->transactionTable->item(row, 0)->data(Qt::UserRole + 1);
- if (projectedData.canConvert<Transaction>()) {
- Transaction t = projectedData.value<Transaction>();
- currentTransactionId = -1; // Will create new actual when saved
- currentProjectedTransaction = t; // Store for conversion
- loadTransactionToEntry(t);
- ui->entryStatusLabel->setText("(Converting Projection to Actual)");
- }
- return;
- }
-
// Load from database
QList<Transaction> allTrans = database->getAllTransactions();
for (const Transaction &t : allTrans) {
if (t.id == id) {
currentTransactionId = id;
loadTransactionToEntry(t);
- ui->entryStatusLabel->setText(QString("Editing ID: %1").arg(id));
+ QString typeLabel = (t.type == TransactionType::Estimated) ? "Estimated" : "Actual";
+ ui->entryStatusLabel->setText(QString("Editing ID: %1 (%2)").arg(id).arg(typeLabel));
return;
}
}
@@ -664,22 +583,29 @@ void CashFlow::onSaveTransaction() {
t.account = ui->entryAccountCombo->currentText();
t.category = ui->entryCategoryCombo->currentText();
t.description = ui->entryDescriptionEdit->text();
- t.type = ui->entryTypeCombo->currentText() == "Actual" ? TransactionType::Actual : TransactionType::Estimated;
+
+ QString typeText = ui->entryTypeCombo->currentText();
+ if (typeText == "Actual") t.type = TransactionType::Actual;
+ else if (typeText == "Reconciliation") t.type = TransactionType::Reconciliation;
+ else t.type = TransactionType::Estimated;
+
+ // If this is a reconciliation checkpoint, calculate the variance
+ if (t.type == TransactionType::Reconciliation) {
+ // amount field holds the expected balance from bank
+ t.expectedBalance = t.amount;
+ // Calculate what we think the balance should be
+ t.calculatedBalance = calculateBalanceUpTo(t.date, t.account);
+ // Don't overwrite user's description - it's saved as-is
+ // Category is irrelevant for reconciliation
+ t.category = "";
+ t.amount = 0; // Reconciliation rows don't affect balance
+ }
// Check if user manually linked to a recurring rule
int manualRuleId = ui->entryRecurringCombo->currentData().toInt();
QString manualOccurrenceKey = ui->entryOccurrenceEdit->text().trimmed();
- // Check if we're converting a projection to actual
- if (currentTransactionId == -1 && currentProjectedTransaction.recurringId != -1) {
- // Converting projection - keep recurring link and store expected values
- t.recurringId = currentProjectedTransaction.recurringId;
- t.occurrenceKey = currentProjectedTransaction.occurrenceKey;
- t.expectedAmount = currentProjectedTransaction.amount;
- t.expectedDate = currentProjectedTransaction.date;
- t.reconciled = true;
- t.type = TransactionType::Actual; // Force to actual when converting
- } else if (manualRuleId != -1 && !manualOccurrenceKey.isEmpty()) {
+ if (manualRuleId != -1 && !manualOccurrenceKey.isEmpty()) {
// User manually linked to recurring rule
t.recurringId = manualRuleId;
t.occurrenceKey = manualOccurrenceKey;
@@ -693,7 +619,7 @@ void CashFlow::onSaveTransaction() {
}
}
} else if (currentTransactionId != -1) {
- // Editing existing transaction - load from database to preserve reconciliation fields
+ // Editing existing transaction - preserve existing reconciliation fields
QList<Transaction> allTrans = database->getAllTransactions();
for (const Transaction &existing : allTrans) {
if (existing.id == currentTransactionId) {
@@ -702,6 +628,16 @@ void CashFlow::onSaveTransaction() {
t.expectedAmount = existing.expectedAmount;
t.expectedDate = existing.expectedDate;
t.reconciled = existing.reconciled;
+ // If user is converting Estimated to Actual, mark as reconciled
+ if (existing.type == TransactionType::Estimated && t.type == TransactionType::Actual) {
+ t.reconciled = true;
+ if (t.expectedAmount == 0) {
+ t.expectedAmount = existing.amount; // Store original projected amount
+ }
+ if (!t.expectedDate.isValid()) {
+ t.expectedDate = existing.date; // Store original projected date
+ }
+ }
break;
}
}
@@ -728,6 +664,9 @@ void CashFlow::onSaveTransaction() {
}
if (success) {
+ // Recalculate all reconciliation checkpoints since balances may have changed
+ recalculateAllReconciliations();
+
ui->entryStatusLabel->setText("Saved!");
refreshView();
} else {
@@ -737,7 +676,6 @@ void CashFlow::onSaveTransaction() {
void CashFlow::onNewTransaction() {
clearTransactionEntry();
- currentProjectedTransaction = Transaction(); // Reset projected transaction
ui->entryDateEdit->setDate(QDate::currentDate());
ui->entryDateEdit->setFocus();
}
@@ -751,6 +689,8 @@ void CashFlow::onDeleteTransaction() {
if (QMessageBox::question(this, "Confirm Delete",
QString("Delete transaction ID %1?").arg(currentTransactionId)) == QMessageBox::Yes) {
if (database->deleteTransaction(currentTransactionId)) {
+ // Recalculate all reconciliation checkpoints since balances may have changed
+ recalculateAllReconciliations();
clearTransactionEntry();
refreshView();
} else {
@@ -759,6 +699,61 @@ void CashFlow::onDeleteTransaction() {
}
}
+void CashFlow::onCreateAdjustment() {
+ // Get the current reconciliation details
+ if (currentTransactionId == -1) return;
+
+ QList<Transaction> allTrans = database->getAllTransactions();
+ Transaction recon;
+ bool found = false;
+
+ for (const Transaction &t : allTrans) {
+ if (t.id == currentTransactionId && t.type == TransactionType::Reconciliation) {
+ recon = t;
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) return;
+
+ double variance = recon.expectedBalance - recon.calculatedBalance;
+
+ QString msg = QString("Create an adjustment transaction for $%1 to match the bank balance?\n\n"
+ "Bank says: $%2\n"
+ "We calculate: $%3\n"
+ "Adjustment needed: $%4")
+ .arg(formatCurrency(qAbs(variance)))
+ .arg(formatCurrency(recon.expectedBalance))
+ .arg(formatCurrency(recon.calculatedBalance))
+ .arg(formatCurrency(variance));
+
+ if (QMessageBox::question(this, "Create Adjustment", msg) == QMessageBox::Yes) {
+ // Create adjustment transaction on the same date, same account
+ Transaction adj;
+ adj.id = -1;
+ adj.date = recon.date;
+ adj.amount = variance;
+ adj.account = recon.account;
+ adj.category = "Adjustment";
+ adj.description = "Balance adjustment to match bank statement";
+ adj.type = TransactionType::Actual;
+ adj.recurringId = -1;
+ adj.reconciled = false;
+ adj.sortOrder = 0; // Doesn't matter - reconciliation always sorts last
+
+ if (database->addTransaction(adj)) {
+ // Recalculate reconciliations and refresh
+ recalculateAllReconciliations();
+ refreshView();
+ QMessageBox::information(this, "Success", "Adjustment transaction created.");
+ ui->adjustmentBtn->setVisible(false); // Hide button since we're now balanced
+ } else {
+ QMessageBox::critical(this, "Error", "Failed to create adjustment: " + database->lastError());
+ }
+ }
+}
+
void CashFlow::onRecurringSelected() {
QList<QTableWidgetItem*> selected = ui->recurringTable->selectedItems();
if (selected.isEmpty()) {
@@ -819,8 +814,20 @@ void CashFlow::onSaveRecurring() {
bool success;
if (currentRecurringId == -1) {
success = database->addRecurringRule(r);
+ if (success) {
+ // Get the newly created rule ID to regenerate projections
+ QList<RecurringRule> rules = database->getAllRecurringRules();
+ if (!rules.isEmpty()) {
+ RecurringRule newRule = rules.last();
+ database->regenerateProjectionsForRule(newRule);
+ }
+ }
} else {
success = database->updateRecurringRule(r);
+ if (success) {
+ // Regenerate projections for updated rule
+ database->regenerateProjectionsForRule(r);
+ }
}
if (success) {
@@ -844,7 +851,10 @@ void CashFlow::onDeleteRecurring() {
}
if (QMessageBox::question(this, "Confirm Delete",
- "Delete this recurring rule?") == QMessageBox::Yes) {
+ "Delete this recurring rule and all its future projections?") == QMessageBox::Yes) {
+ // Delete future projections first
+ database->deleteProjectionsForRule(currentRecurringId);
+ // Then delete the rule
if (database->deleteRecurringRule(currentRecurringId)) {
clearRecurringEntry();
refreshView();
@@ -881,11 +891,19 @@ void CashFlow::loadTransactionToEntry(const Transaction &t) {
ui->entryOccurrenceEdit->blockSignals(true);
ui->entryDateEdit->setDate(t.date);
- ui->entryAmountSpin->setValue(t.amount);
+
+ // For reconciliation rows, load the expected balance into amount field
+ double loadAmount = (t.type == TransactionType::Reconciliation) ? t.expectedBalance : t.amount;
+ ui->entryAmountSpin->setValue(loadAmount);
+
ui->entryAccountCombo->setCurrentText(t.account);
ui->entryCategoryCombo->setCurrentText(t.category);
ui->entryDescriptionEdit->setText(t.description);
- ui->entryTypeCombo->setCurrentIndex(t.type == TransactionType::Actual ? 1 : 0);
+
+ int typeIndex = 0; // Estimated
+ if (t.type == TransactionType::Actual) typeIndex = 1;
+ else if (t.type == TransactionType::Reconciliation) typeIndex = 2;
+ ui->entryTypeCombo->setCurrentIndex(typeIndex);
// Set recurring rule link if present
if (t.recurringId != -1) {
@@ -913,6 +931,14 @@ void CashFlow::loadTransactionToEntry(const Transaction &t) {
ui->entryRecurringCombo->blockSignals(false);
ui->entryOccurrenceEdit->blockSignals(false);
+ // Show adjustment button if this is a reconciliation that's out of balance
+ if (t.type == TransactionType::Reconciliation) {
+ double variance = qAbs(t.expectedBalance - t.calculatedBalance);
+ ui->adjustmentBtn->setVisible(variance >= 0.01);
+ } else {
+ ui->adjustmentBtn->setVisible(false);
+ }
+
updateAmountColors();
}
@@ -977,10 +1003,20 @@ void CashFlow::loadSettings() {
bool showAccountBalances = database->getSetting("show_account_balances", "0").toInt();
weekStartDay = database->getSetting("week_start_day", "1").toInt();
+ // Load date range
+ QDate today = QDate::currentDate();
+ QString dateFromStr = database->getSetting("date_from", "");
+ QString dateToStr = database->getSetting("date_to", "");
+
+ QDate dateFrom = dateFromStr.isEmpty() ? QDate(today.year(), today.month(), 1) : QDate::fromString(dateFromStr, Qt::ISODate);
+ QDate dateTo = dateToStr.isEmpty() ? today.addMonths(3) : QDate::fromString(dateToStr, Qt::ISODate);
+
// Apply to member variables and main UI
currentAmountFont = QFont(fontFamily, fontSize);
ui->periodCombo->setCurrentIndex(defaultPeriod);
ui->showAccountBalancesCheck->setChecked(showAccountBalances);
+ ui->dateFromEdit->setDate(dateFrom);
+ ui->dateToEdit->setDate(dateTo);
}
QString CashFlow::formatCurrency(double amount) const {
@@ -988,6 +1024,40 @@ QString CashFlow::formatCurrency(double amount) const {
return locale.toString(amount, 'f', 2);
}
+double CashFlow::calculateBalanceUpTo(const QDate &date, const QString &account) {
+ QList<Transaction> allTrans = database->getAllTransactions();
+ double balance = 0.0;
+
+ // Sort by date to ensure proper ordering
+ std::sort(allTrans.begin(), allTrans.end(), [](const Transaction &a, const Transaction &b) {
+ if (a.date != b.date) return a.date < b.date;
+ return a.sortOrder < b.sortOrder;
+ });
+
+ for (const Transaction &t : allTrans) {
+ if (t.date > date) continue;
+ if (!account.isEmpty() && t.account != account) continue;
+ if (t.type == TransactionType::Reconciliation) continue; // Don't count reconciliation rows
+
+ balance += t.amount;
+ }
+
+ return balance;
+}
+
+void CashFlow::recalculateAllReconciliations() {
+ QList<Transaction> allTrans = database->getAllTransactions();
+
+ for (Transaction &t : allTrans) {
+ if (t.type == TransactionType::Reconciliation) {
+ // Recalculate the calculated balance
+ t.calculatedBalance = calculateBalanceUpTo(t.date, t.account);
+ // Update in database
+ database->updateTransaction(t);
+ }
+ }
+}
+
bool CashFlow::openDatabase(const QString &filePath) {
if (database->open(filePath)) {
currentFilePath = filePath;
@@ -996,11 +1066,6 @@ bool CashFlow::openDatabase(const QString &filePath) {
loadSettings();
- // Set default date range (current month to 3 months out)
- QDate today = QDate::currentDate();
- ui->dateFromEdit->setDate(QDate(today.year(), today.month(), 1));
- ui->dateToEdit->setDate(today.addMonths(3));
-
clearTransactionEntry();
clearRecurringEntry();
refreshView();